Updated on 2026-08-14
This commit is contained in:
commit
707f04c6a5
232 changed files with 7456 additions and 1908 deletions
|
|
@ -7,7 +7,10 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
|
|
@ -42,6 +45,17 @@ abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackP
|
|||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
loadToolbarMenu()?.let {
|
||||
val menuHost = requireActivity() as? MenuHost ?: return
|
||||
menuHost.addMenuProvider(it, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun loadToolbarMenu():MenuProvider? = null
|
||||
|
||||
fun showRetrySnackbar(message: String, action: VoidCallback) {
|
||||
val snackbar = Snackbar.make(mainView, message, Snackbar.LENGTH_INDEFINITE)
|
||||
snackbar.setAction(getString(R.string.common_retry)) {
|
||||
|
|
|
|||
|
|
@ -448,6 +448,33 @@ class DemoConfig {
|
|||
"AB02000000049533",
|
||||
"AB02000000049541",
|
||||
"AB02000000049830",
|
||||
// === more cids ===
|
||||
"AC03000000091418",
|
||||
"AC03000000091400",
|
||||
"AC03000000099007",
|
||||
"AC03000000098991",
|
||||
"AC03000000098942",
|
||||
"AC03000000091715",
|
||||
"AC03000000091301",
|
||||
"AC03000000091343",
|
||||
"AB01000000055705",
|
||||
"AB01000000052918",
|
||||
"AB01000000047710",
|
||||
"AB01000000052306",
|
||||
"AB01000000047645",
|
||||
"AB01000000048957",
|
||||
"AB01000000052900",
|
||||
"AB01000000050391",
|
||||
"AB01000000047363",
|
||||
"AB02000000053998",
|
||||
"AB02000000019809",
|
||||
"AB02000000020872",
|
||||
"AB02000000022027",
|
||||
"AB02000000058955",
|
||||
"AB02000000053253",
|
||||
"AB02000000048063",
|
||||
"AB02000000023736",
|
||||
"AB02000000058187",
|
||||
)
|
||||
|
||||
private val testDemoCardIds = listOf(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.features.details.redux
|
|||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.common.isTangemTwins
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.tap.common.analytics.Analytics
|
||||
import com.tangem.tap.common.analytics.AnalyticsParam
|
||||
import com.tangem.tap.common.extensions.dispatchNotification
|
||||
|
|
@ -69,13 +69,8 @@ class DetailsMiddleware {
|
|||
}
|
||||
is DetailsAction.CreateBackup -> {
|
||||
store.state.detailsState.scanResponse?.let {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
|
||||
store.dispatch(
|
||||
GlobalAction.Onboarding.Start(
|
||||
it,
|
||||
fromHomeScreen = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
DetailsAction.ScanCard -> {
|
||||
|
|
@ -98,7 +93,7 @@ class DetailsMiddleware {
|
|||
when (action) {
|
||||
is DetailsAction.ResetToFactory.Start -> {
|
||||
val card = store.state.detailsState.cardSettingsState?.card ?: return
|
||||
if (card.isTangemTwins()) {
|
||||
if (card.isTangemTwins) {
|
||||
store.dispatch(DetailsAction.ReCreateTwinsWallet)
|
||||
return
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ private fun prepareSecurityOptions(card: Card): ManageSecurityState {
|
|||
}
|
||||
}
|
||||
val allowedSecurityOptions = when {
|
||||
card.isStart2Coin || card.isTangemNote() -> {
|
||||
card.isStart2Coin || card.isTangemNote || card.isSaltPay -> {
|
||||
EnumSet.of(SecurityOption.LongTap)
|
||||
}
|
||||
card.settings.isBackupAllowed -> {
|
||||
|
|
@ -97,7 +97,7 @@ private fun prepareSecurityOptions(card: Card): ManageSecurityState {
|
|||
private fun isResetToFactoryAllowedByCard(card: Card): Boolean {
|
||||
val notAllowedByAnyWallet = card.wallets.any { it.settings.isPermanent }
|
||||
val notAllowedByCard = notAllowedByAnyWallet ||
|
||||
(card.isWalletDataSupported && (!card.isTangemNote() && !card.settings.isBackupAllowed)) ||
|
||||
(card.isWalletDataSupported && (!card.isTangemNote && !card.settings.isBackupAllowed)) ||
|
||||
card.isSaltPay
|
||||
return !notAllowedByCard
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.features.details.redux.walletconnect
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.redux.NotificationAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.wallet.R
|
||||
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
|
||||
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
|
||||
|
|
@ -52,7 +53,7 @@ sealed class WalletConnectAction : Action {
|
|||
val updatedSession: WalletConnectSession,
|
||||
) : WalletConnectAction()
|
||||
|
||||
data class FailureEstablishingSession(val session: WCSession?) : WalletConnectAction()
|
||||
data class FailureEstablishingSession(val session: WCSession?, val error: TapError? = null) : WalletConnectAction()
|
||||
data class SetSessionsRestored(val sessions: List<WalletConnectSession>) :
|
||||
WalletConnectAction()
|
||||
|
||||
|
|
@ -103,6 +104,6 @@ sealed class WalletConnectAction : Action {
|
|||
|
||||
data class Sign(
|
||||
val id: Long, val data: ByteArray, val sessionData: WCSession,
|
||||
) : WalletConnectAction()
|
||||
) : WalletConnectAction()
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import com.tangem.domain.common.ScanResponse
|
|||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
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
|
||||
|
|
@ -22,6 +23,7 @@ import com.tangem.tap.features.demo.DemoHelper
|
|||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
|
|
@ -96,6 +98,16 @@ class WalletConnectMiddleware {
|
|||
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout))
|
||||
}
|
||||
is WalletConnectAction.FailureEstablishingSession -> {
|
||||
if (action.error != null) {
|
||||
store.dispatch(
|
||||
GlobalAction.ShowDialog(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_warning,
|
||||
messageId = action.error.messageResource,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (action.session != null) {
|
||||
walletConnectManager.disconnect(action.session)
|
||||
}
|
||||
|
|
@ -306,9 +318,11 @@ class WalletConnectMiddleware {
|
|||
} else {
|
||||
blockchain
|
||||
}
|
||||
val derivation = blockchainToMake.derivationPath(store.state.globalState.scanResponse?.card?.derivationStyle)
|
||||
?.rawPath
|
||||
val blockchainNetwork = BlockchainNetwork(
|
||||
blockchain = blockchainToMake,
|
||||
derivationPath = wallet.derivationPath?.rawPath,
|
||||
derivationPath = derivation,
|
||||
tokens = emptyList(),
|
||||
)
|
||||
return walletState.getWalletManager(blockchainNetwork)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.features.details.ui.cardsettings
|
||||
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.getTwinCardIdForUser
|
||||
import com.tangem.domain.common.isTangemTwins
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.details.redux.CardSettingsState
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
|
|
@ -20,7 +20,7 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
|
|||
},
|
||||
)
|
||||
} else {
|
||||
val cardId = if (state.card.isTangemTwins()) {
|
||||
val cardId = if (state.card.isTangemTwins) {
|
||||
state.card.getTwinCardIdForUser()
|
||||
} else {
|
||||
state.cardInfo.cardId
|
||||
|
|
@ -30,7 +30,7 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
|
|||
CardInfo.Issuer(state.cardInfo.issuer),
|
||||
)
|
||||
|
||||
if (!state.card.isTangemTwins()) {
|
||||
if (!state.card.isTangemTwins) {
|
||||
cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString()))
|
||||
}
|
||||
cardDetails.add(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
|
@ -86,7 +87,7 @@ fun WalletConnectDetailsItem(
|
|||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.height(84.dp)
|
||||
.defaultMinSize(minHeight = 84.dp)
|
||||
.fillMaxWidth()
|
||||
.clickable { onItemsClick(SettingsElement.WalletConnect) },
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
|
|
@ -99,7 +100,7 @@ fun WalletConnectDetailsItem(
|
|||
tint = colorResource(id = R.color.all_colors_azure),
|
||||
)
|
||||
Column(
|
||||
modifier = modifier.height(56.dp),
|
||||
modifier = modifier.defaultMinSize(minHeight = 56.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.details.ui.details
|
||||
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.tap.common.feedback.FeedbackEmail
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
|
|
@ -25,7 +26,9 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
|||
if (state.scanResponse?.card?.isMultiwalletAllowed == true) it else null
|
||||
}
|
||||
SettingsElement.LinkMoreCards -> {
|
||||
if (state.createBackupAllowed) it else null
|
||||
// if (state.createBackupAllowed) it else null
|
||||
// TODO: SaltPay: temporary excluding backup process for Visa cards
|
||||
if (state.createBackupAllowed && state.scanResponse?.card?.isSaltPay != true) it else null
|
||||
}
|
||||
SettingsElement.PrivacyPolicy -> {
|
||||
if (state.privacyPolicyUrl != null) it else null
|
||||
|
|
@ -75,8 +78,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
|||
store.dispatch(DetailsAction.CreateBackup)
|
||||
}
|
||||
SettingsElement.TermsOfService -> {
|
||||
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
|
||||
store.dispatch(DisclaimerAction.Show())
|
||||
}
|
||||
SettingsElement.TermsOfUse -> {
|
||||
store.dispatch(DetailsAction.ShowDisclaimer)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.tap.features.details.ui.resetcard
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -89,11 +91,14 @@ fun ResetCardView(
|
|||
color = colorResource(id = R.color.text_secondary),
|
||||
)
|
||||
|
||||
Spacer(modifier = modifier.size(44.dp))
|
||||
Spacer(modifier = modifier.size(28.dp))
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 20.dp),
|
||||
.clickable(
|
||||
onClick = { state.onAcceptWarningToggleClick(!state.accepted) },
|
||||
)
|
||||
.padding(top = 16.dp, bottom = 16.dp),
|
||||
) {
|
||||
IconToggleButton(
|
||||
checked = state.accepted,
|
||||
|
|
@ -120,13 +125,15 @@ fun ResetCardView(
|
|||
text = stringResource(id = R.string.reset_card_to_factory_warning_message),
|
||||
style = TangemTypography.body2,
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
modifier = modifier
|
||||
.padding(end = 20.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = modifier.size(32.dp))
|
||||
Spacer(modifier = modifier.size(16.dp))
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(start = 16.dp, end = 16.dp),
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 32.dp),
|
||||
) {
|
||||
DetailsMainButton(
|
||||
title = stringResource(id = R.string.reset_card_to_factory_button_title),
|
||||
|
|
|
|||
|
|
@ -140,13 +140,12 @@ private fun WalletConnectSessions(
|
|||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Text(
|
||||
text = session.description,
|
||||
style = TangemTypography.subtitle1,
|
||||
color = colorResource(id = R.color.text_primary_1),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = session.description,
|
||||
style = TangemTypography.subtitle1,
|
||||
color = colorResource(id = R.color.text_primary_1),
|
||||
modifier = modifier.weight(1f),
|
||||
)
|
||||
IconButton(
|
||||
onClick = { state.onRemoveSession(session.sessionId) },
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
package com.tangem.tap.features.disclaimer.redux
|
||||
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class DisclaimerAction: Action {
|
||||
object AcceptDisclaimer : DisclaimerAction()
|
||||
object ShowAcceptedDisclaimer : DisclaimerAction()
|
||||
sealed class DisclaimerAction : Action {
|
||||
|
||||
data class SetDisclaimerType(
|
||||
val type: DisclaimerType,
|
||||
) : DisclaimerAction()
|
||||
|
||||
data class Show(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction()
|
||||
data class AcceptDisclaimer(val type: DisclaimerType) : DisclaimerAction()
|
||||
|
||||
internal data class UpdateState(val type: DisclaimerType, val accepted: Boolean) : DisclaimerAction()
|
||||
internal data class SetOnAcceptCallback(val onAcceptCallback: VoidCallback? = null) : DisclaimerAction()
|
||||
}
|
||||
|
|
@ -1,28 +1,65 @@
|
|||
package com.tangem.tap.features.disclaimer.redux
|
||||
|
||||
import com.tangem.tap.common.post
|
||||
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.features.home.redux.HomeAction
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
class DisclaimerMiddleware {
|
||||
val disclaimerMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
is DisclaimerAction.AcceptDisclaimer -> {
|
||||
preferencesStorage.saveDisclaimerAccepted()
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
|
||||
//delayed sending used for better animation
|
||||
post(550) { store.dispatch(HomeAction.ReadCard) }
|
||||
}
|
||||
}
|
||||
state()?.let { handleDisclaimerMiddleware(action, it) }
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDisclaimerMiddleware(action: Action, appState: AppState) {
|
||||
val state = appState.disclaimerState
|
||||
|
||||
when (action) {
|
||||
is DisclaimerAction.SetDisclaimerType -> {
|
||||
handleUpdateState = action.type.createUpdateState()
|
||||
}
|
||||
is DisclaimerAction.Show -> {
|
||||
handleUpdateState = state.type.createUpdateState()
|
||||
store.dispatch(DisclaimerAction.SetOnAcceptCallback(action.onAcceptCallback))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
|
||||
}
|
||||
is DisclaimerAction.AcceptDisclaimer -> {
|
||||
action.type.accept()
|
||||
handleUpdateState = action.type.createUpdateState()
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
state.onAcceptCallback?.invoke()
|
||||
store.dispatch(DisclaimerAction.SetOnAcceptCallback(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DisclaimerType.accept() = when (this) {
|
||||
DisclaimerType.SaltPay -> preferencesStorage.disclaimerPrefStorage.hasSaltPayTosAccepted = true
|
||||
DisclaimerType.Tangem -> preferencesStorage.disclaimerPrefStorage.hasTangemTosAccepted = true
|
||||
}
|
||||
|
||||
fun DisclaimerType.isAccepted(): Boolean = when (this) {
|
||||
DisclaimerType.SaltPay -> preferencesStorage.disclaimerPrefStorage.hasSaltPayTosAccepted
|
||||
DisclaimerType.Tangem -> preferencesStorage.disclaimerPrefStorage.hasTangemTosAccepted
|
||||
}
|
||||
|
||||
private fun DisclaimerType.createUpdateState(): DisclaimerAction.UpdateState {
|
||||
return DisclaimerAction.UpdateState(this, this.isAccepted())
|
||||
}
|
||||
|
||||
private var handleUpdateState: DisclaimerAction.UpdateState = DisclaimerAction.UpdateState(
|
||||
type = DisclaimerType.Tangem,
|
||||
accepted = false,
|
||||
)
|
||||
set(value) {
|
||||
field = value
|
||||
store.dispatch(value)
|
||||
}
|
||||
|
|
@ -13,9 +13,15 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState {
|
|||
if (action !is DisclaimerAction) return state.disclaimerState
|
||||
|
||||
val disclaimerState = state.disclaimerState
|
||||
|
||||
return when (action) {
|
||||
is DisclaimerAction.AcceptDisclaimer, DisclaimerAction.ShowAcceptedDisclaimer -> {
|
||||
disclaimerState.copy(accepted = true)
|
||||
}
|
||||
is DisclaimerAction.UpdateState -> disclaimerState.copy(
|
||||
type = action.type,
|
||||
accepted = action.accepted,
|
||||
)
|
||||
is DisclaimerAction.SetOnAcceptCallback -> disclaimerState.copy(
|
||||
onAcceptCallback = action.onAcceptCallback,
|
||||
)
|
||||
else -> disclaimerState
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,30 @@
|
|||
package com.tangem.tap.features.disclaimer.redux
|
||||
|
||||
import android.net.Uri
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class DisclaimerState(
|
||||
val accepted: Boolean = false
|
||||
val accepted: Boolean = false,
|
||||
val type: DisclaimerType = DisclaimerType.Tangem,
|
||||
val onAcceptCallback: VoidCallback? = null,
|
||||
) : StateType
|
||||
|
||||
sealed class DisclaimerType(
|
||||
val uri: Uri,
|
||||
) {
|
||||
object Tangem : DisclaimerType(Uri.parse("https://tangem.com/tangem_tos.html"))
|
||||
object SaltPay : DisclaimerType(Uri.parse("https://tangem.com/soltpay_tos.html"))
|
||||
|
||||
companion object {
|
||||
fun get(scanResponse: ScanResponse): DisclaimerType = get(scanResponse.card)
|
||||
|
||||
fun get(card: Card): DisclaimerType = when {
|
||||
card.isSaltPay -> SaltPay
|
||||
else -> Tangem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.activity.OnBackPressedCallback
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.transition.TransitionInflater
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.tangem.tap.common.extensions.configureSettings
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
|
|
@ -16,17 +17,20 @@ import com.tangem.wallet.databinding.FragmentDisclaimerBinding
|
|||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer),
|
||||
StoreSubscriber<DisclaimerState> {
|
||||
StoreSubscriber<DisclaimerState> {
|
||||
|
||||
private val binding: FragmentDisclaimerBinding by viewBinding(FragmentDisclaimerBinding::bind)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
})
|
||||
activity?.onBackPressedDispatcher?.addCallback(
|
||||
this,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
},
|
||||
)
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
|
||||
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
|
||||
|
|
@ -51,19 +55,25 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer),
|
|||
binding.toolbar.setNavigationOnClickListener {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
setOnClickListeners()
|
||||
initViews()
|
||||
}
|
||||
|
||||
private fun setOnClickListeners() {
|
||||
binding.btnAccept.setOnClickListener { store.dispatch(DisclaimerAction.AcceptDisclaimer) }
|
||||
private fun initViews() {
|
||||
binding.webView.configureSettings()
|
||||
}
|
||||
|
||||
override fun newState(state: DisclaimerState) {
|
||||
override fun newState(state: DisclaimerState) = with(binding) {
|
||||
if (activity == null || view == null) return
|
||||
|
||||
if (state.accepted) {
|
||||
binding.halfTransparentOverlay.hide()
|
||||
binding.btnAccept.hide()
|
||||
halfTransparentOverlay.hide()
|
||||
btnAccept.hide()
|
||||
}
|
||||
|
||||
btnAccept.setOnClickListener {
|
||||
store.dispatch(DisclaimerAction.AcceptDisclaimer(state.type))
|
||||
}
|
||||
|
||||
webView.loadUrl(state.type.uri.toString())
|
||||
}
|
||||
}
|
||||
|
|
@ -81,16 +81,12 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
|
|||
StoriesScreen(
|
||||
homeState,
|
||||
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
|
||||
onShopButtonClick = {
|
||||
store.dispatch(
|
||||
HomeAction.GoToShop(store.state.globalState.userCountryCode)
|
||||
)
|
||||
},
|
||||
onShopButtonClick = { store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode)) },
|
||||
onSearchTokensClick = {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
|
||||
store.dispatch(TokensAction.AllowToAddTokens(false))
|
||||
store.dispatch(TokensAction.LoadCurrencies())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ import com.tangem.tap.features.home.compose.content.StoriesWeb3
|
|||
import com.tangem.tap.features.home.compose.views.HomeButtons
|
||||
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
|
||||
import com.tangem.tap.features.home.redux.HomeState
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.wallet.R
|
||||
import kotlin.math.max
|
||||
|
||||
|
|
@ -74,7 +73,7 @@ fun StoriesScreen(
|
|||
}
|
||||
|
||||
val isPressed = remember { mutableStateOf(false) }
|
||||
val isPaused = isPressed.value || homeState.value.btnScanState.progressState == ProgressState.Loading
|
||||
val isPaused = isPressed.value || homeState.value.scanInProgress
|
||||
|
||||
val hideContent = remember { mutableStateOf(true) }
|
||||
|
||||
|
|
@ -88,10 +87,10 @@ fun StoriesScreen(
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFF090E13))
|
||||
.background(Color(0xFF090E13)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
|
|
@ -109,7 +108,7 @@ fun StoriesScreen(
|
|||
isPressed.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
|
|
@ -127,7 +126,7 @@ fun StoriesScreen(
|
|||
isPressed.value = false
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
if (!isDarkBackground) {
|
||||
|
|
@ -135,7 +134,7 @@ fun StoriesScreen(
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
painter = painterResource(id = R.drawable.ic_overlay),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillBounds
|
||||
contentScale = ContentScale.FillBounds,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +164,7 @@ fun StoriesScreen(
|
|||
.height(17.dp)
|
||||
.alpha(if (hideContent.value) 0f else 1f)
|
||||
.align(Alignment.Start),
|
||||
colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black)
|
||||
colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black),
|
||||
)
|
||||
when (currentStep.value) {
|
||||
1 -> FirstStoriesContent(isPaused, currentStep.duration()) { hideContent.value = it }
|
||||
|
|
@ -190,27 +189,28 @@ fun StoriesScreen(
|
|||
.height(48.dp),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
backgroundColor = Color.White,
|
||||
contentColor = Color(0xFF080C10)
|
||||
)
|
||||
contentColor = Color(0xFF080C10),
|
||||
),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_search),
|
||||
contentDescription = null
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.search_tokens_title),
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
HomeButtons(
|
||||
isDarkBackground = isDarkBackground,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, top = 0.dp, end = 16.dp, bottom = 37.dp)
|
||||
.fillMaxWidth(),
|
||||
isDarkBackground = isDarkBackground,
|
||||
btnScanStateInProgress = homeState.value.btnScanStateInProgress,
|
||||
onScanButtonClick = onScanButtonClick,
|
||||
onShopButtonClick = onShopButtonClick
|
||||
onShopButtonClick = onShopButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ package com.tangem.tap.features.home.compose.views
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -19,53 +21,89 @@ import com.tangem.wallet.R
|
|||
|
||||
@Composable
|
||||
fun HomeButtons(
|
||||
isDarkBackground: Boolean, modifier: Modifier = Modifier,
|
||||
modifier: Modifier = Modifier,
|
||||
isDarkBackground: Boolean,
|
||||
btnScanStateInProgress: Boolean,
|
||||
onScanButtonClick: () -> Unit,
|
||||
onShopButtonClick: () -> Unit
|
||||
onShopButtonClick: () -> Unit,
|
||||
) {
|
||||
val darkColorBackground = Color(0xFF26292E)
|
||||
val darkColorButton = Color(0xFF080C10)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
) {
|
||||
Button(
|
||||
ProgressButton(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
inProgress = btnScanStateInProgress,
|
||||
backgroundColor = if (isDarkBackground) darkColorBackground else Color.White,
|
||||
contentColor = if (isDarkBackground) Color.White else darkColorButton,
|
||||
onClick = onScanButtonClick,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
backgroundColor = if (isDarkBackground) darkColorBackground else Color.White,
|
||||
contentColor = if (isDarkBackground) Color.White else darkColorButton
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.home_button_scan),
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
progressContent = {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(30.dp),
|
||||
color = if (isDarkBackground) Color.White else darkColorBackground,
|
||||
strokeWidth = 3.dp,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.home_button_scan),
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
)
|
||||
SpacerS8()
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
onClick = onShopButtonClick,
|
||||
enabled = !btnScanStateInProgress,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
backgroundColor = if (isDarkBackground) Color.White else darkColorBackground,
|
||||
contentColor = if (isDarkBackground) darkColorButton else Color.White
|
||||
)
|
||||
contentColor = if (isDarkBackground) darkColorButton else Color.White,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.home_button_order),
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProgressButton(
|
||||
modifier: Modifier,
|
||||
backgroundColor: Color,
|
||||
contentColor: Color,
|
||||
onClick: () -> Unit,
|
||||
inProgress: Boolean,
|
||||
progressContent: @Composable (() -> Unit)? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
backgroundColor = backgroundColor,
|
||||
contentColor = contentColor,
|
||||
),
|
||||
) {
|
||||
if (progressContent != null && inProgress) {
|
||||
progressContent()
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,13 +6,12 @@ import org.rekotlin.Action
|
|||
sealed class HomeAction : Action {
|
||||
// from ui
|
||||
object ReadCard : HomeAction()
|
||||
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
|
||||
data class GoToShop(val userCountryCode: String?) : HomeAction()
|
||||
|
||||
// internal
|
||||
data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction()
|
||||
object Init : HomeAction()
|
||||
|
||||
data class SetTermsOfUseState(val isDisclaimerAccepted: Boolean) : HomeAction()
|
||||
data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction()
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,47 @@
|
|||
package com.tangem.tap.features.home.redux
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.extensions.withIOContext
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
|
||||
import com.tangem.tap.common.analytics.AnalyticsEvent
|
||||
import com.tangem.tap.common.analytics.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.GetCardSourceParams
|
||||
import com.tangem.tap.common.entities.IndeterminateProgressButton
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.post
|
||||
import com.tangem.tap.common.postUi
|
||||
import com.tangem.tap.common.postUiDelayBg
|
||||
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.FragmentShareTransition
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerType
|
||||
import com.tangem.tap.features.disclaimer.redux.isAccepted
|
||||
import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
import com.tangem.tap.features.send.redux.states.ButtonState
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
class HomeMiddleware {
|
||||
|
|
@ -36,87 +52,146 @@ class HomeMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private val homeMiddleware: Middleware<AppState> = { _, _ ->
|
||||
private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
is HomeAction.Init -> {
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
store.dispatch(GlobalAction.ExchangeManager.Init)
|
||||
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
|
||||
store.dispatch(GlobalAction.FetchUserCountry)
|
||||
}
|
||||
is HomeAction.ShouldScanCardOnResume -> {
|
||||
if (action.shouldScanCard) {
|
||||
store.dispatch(HomeAction.ShouldScanCardOnResume(false))
|
||||
postUi(700) { store.dispatch(HomeAction.ReadCard) }
|
||||
}
|
||||
}
|
||||
is HomeAction.ReadCard -> {
|
||||
handleReadCard()
|
||||
// store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomTokens))
|
||||
}
|
||||
is HomeAction.GoToShop -> {
|
||||
when (action.userCountryCode) {
|
||||
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE ->
|
||||
store.dispatchOpenUrl(BUY_WALLET_URL)
|
||||
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
|
||||
}
|
||||
store.state.globalState.analyticsHandler?.handleAnalyticsEvent(
|
||||
event = AnalyticsEvent.GET_CARD,
|
||||
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param)
|
||||
)
|
||||
}
|
||||
}
|
||||
handleHomeAction(state, action, dispatch)
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReadCard() {
|
||||
if (!preferencesStorage.wasDisclaimerAccepted()) {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
|
||||
} else {
|
||||
changeButtonState(ButtonState.PROGRESS)
|
||||
store.dispatch(
|
||||
GlobalAction.ScanCard(
|
||||
onSuccess = { scanResponse ->
|
||||
store.state.globalState.tapWalletManager.updateConfigManager(scanResponse)
|
||||
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
|
||||
private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch: DispatchFunction) {
|
||||
when (action) {
|
||||
is HomeAction.Init -> {
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
store.dispatch(GlobalAction.ExchangeManager.Init)
|
||||
store.dispatch(GlobalAction.FetchUserCountry)
|
||||
}
|
||||
is HomeAction.ShouldScanCardOnResume -> {
|
||||
if (action.shouldScanCard) {
|
||||
store.dispatch(HomeAction.ShouldScanCardOnResume(false))
|
||||
postUiDelayBg(700) { store.dispatch(HomeAction.ReadCard) }
|
||||
}
|
||||
}
|
||||
is HomeAction.ReadCard -> {
|
||||
changeButtonState(ButtonState.PROGRESS)
|
||||
val scanCardAction = GlobalAction.ScanCard(
|
||||
onSuccess = {
|
||||
store.dispatch(HomeAction.ScanInProgress(false))
|
||||
checkForDisclaimer(it, ::onScanSuccess)
|
||||
},
|
||||
onFailure = ::onScanFailure,
|
||||
)
|
||||
store.dispatch(HomeAction.ScanInProgress(true))
|
||||
postUiDelayBg(300) { store.dispatch(scanCardAction) }
|
||||
}
|
||||
is HomeAction.GoToShop -> {
|
||||
when (action.userCountryCode) {
|
||||
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL)
|
||||
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
|
||||
}
|
||||
store.state.globalState.analyticsHandlers?.triggerEvent(
|
||||
event = AnalyticsEvent.GET_CARD,
|
||||
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
|
||||
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
|
||||
store.dispatch(GlobalAction.Onboarding.Start(scanResponse))
|
||||
navigateTo(navigateTo)
|
||||
} else {
|
||||
scope.launch {
|
||||
withMainContext {
|
||||
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
|
||||
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
|
||||
navigateTo(AppScreen.OnboardingTwins)
|
||||
} else {
|
||||
navigateTo(AppScreen.Wallet, null)
|
||||
}
|
||||
}
|
||||
store.onCardScanned(scanResponse)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
},
|
||||
),
|
||||
private fun checkForDisclaimer(scanResponse: ScanResponse, onDisclaimerAccepted: (ScanResponse) -> Unit) {
|
||||
val disclaimerType = DisclaimerType.get(scanResponse)
|
||||
store.dispatch(DisclaimerAction.SetDisclaimerType(disclaimerType))
|
||||
|
||||
if (disclaimerType.isAccepted()) {
|
||||
onDisclaimerAccepted((scanResponse))
|
||||
} else {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
store.dispatch(
|
||||
DisclaimerAction.Show {
|
||||
changeButtonState(ButtonState.PROGRESS)
|
||||
onDisclaimerAccepted(scanResponse)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanSuccess(scanResponse: ScanResponse) {
|
||||
val globalState = store.state.globalState
|
||||
val tapWalletManager = globalState.tapWalletManager
|
||||
tapWalletManager.updateConfigManager(scanResponse)
|
||||
|
||||
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
|
||||
|
||||
if (scanResponse.isSaltPay()) {
|
||||
if (scanResponse.isSaltPayVisa()) {
|
||||
scope.launch {
|
||||
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
|
||||
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager)
|
||||
delay(500)
|
||||
withMainContext {
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val isOnboardingCase = result.data
|
||||
if (isOnboardingCase) {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
|
||||
store.dispatch(OnboardingSaltPayAction.Init.SetDependencies(manager, config))
|
||||
store.dispatch(OnboardingSaltPayAction.Update)
|
||||
navigateTo(AppScreen.OnboardingWallet)
|
||||
} else {
|
||||
navigateTo(AppScreen.Wallet)
|
||||
withIOContext { store.onCardScanned(scanResponse) }
|
||||
}
|
||||
}
|
||||
is Result.Failure -> {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
SaltPayExceptionHandler.handle(result.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (scanResponse.card.backupStatus?.isActive == false) {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.saltpay_error_empty_backup_title,
|
||||
messageId = R.string.saltpay_error_empty_backup_message,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
navigateTo(AppScreen.Wallet, null)
|
||||
scope.launch { store.onCardScanned(scanResponse) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
|
||||
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
|
||||
navigateTo(appScreen)
|
||||
} else {
|
||||
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
|
||||
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
|
||||
navigateTo(AppScreen.OnboardingTwins)
|
||||
} else {
|
||||
navigateTo(AppScreen.Wallet, null)
|
||||
}
|
||||
scope.launch { store.onCardScanned(scanResponse) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanFailure(error: TangemError) {
|
||||
store.dispatch(HomeAction.ScanInProgress(false))
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
}
|
||||
|
||||
private fun changeButtonState(state: ButtonState) {
|
||||
val btnState = IndeterminateProgressButton(state)
|
||||
store.dispatch(HomeAction.ChangeScanCardButtonState(btnState))
|
||||
store.dispatch(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
|
||||
}
|
||||
|
||||
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition? = null) {
|
||||
post(DELAY_SDK_DIALOG_CLOSE) {
|
||||
postUiDelayBg(DELAY_SDK_DIALOG_CLOSE) {
|
||||
changeButtonState(ButtonState.ENABLED)
|
||||
store.dispatch(NavigationAction.NavigateTo(screen, transition))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ private fun internalReduce(action: Action, state: AppState): HomeState {
|
|||
|
||||
var state = state.homeState
|
||||
when (action) {
|
||||
is HomeAction.ScanInProgress -> {
|
||||
state = state.copy(scanInProgress = action.scanInProgress)
|
||||
}
|
||||
is HomeAction.ShouldScanCardOnResume -> {
|
||||
state = state.copy(shouldScanCardOnResume = action.shouldScanCard)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,15 @@ package com.tangem.tap.features.home.redux
|
|||
|
||||
import com.tangem.tap.common.entities.IndeterminateProgressButton
|
||||
import com.tangem.tap.features.send.redux.states.ButtonState
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class HomeState(
|
||||
val shouldScanCardOnResume: Boolean = false,
|
||||
val scanInProgress: Boolean = false,
|
||||
val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED),
|
||||
) : StateType
|
||||
) : StateType {
|
||||
|
||||
val btnScanStateInProgress: Boolean
|
||||
get() = btnScanState.progressState == ProgressState.Loading
|
||||
}
|
||||
|
|
@ -15,16 +15,14 @@ class OnboardingHelper {
|
|||
fun isOnboardingCase(response: ScanResponse): Boolean {
|
||||
val cardInfoStorage = preferencesStorage.usedCardsPrefStorage
|
||||
return when {
|
||||
response.productType == ProductType.Twins -> {
|
||||
response.isTangemTwins() -> {
|
||||
if (!response.twinsIsTwinned()) {
|
||||
true
|
||||
} else {
|
||||
cardInfoStorage.activationIsStarted(response.card.cardId)
|
||||
}
|
||||
}
|
||||
response.card.hasWallets() -> {
|
||||
cardInfoStorage.activationIsStarted(response.card.cardId)
|
||||
}
|
||||
response.card.hasWallets() -> cardInfoStorage.activationIsStarted(response.card.cardId)
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +36,7 @@ class OnboardingHelper {
|
|||
AppScreen.OnboardingOther
|
||||
}
|
||||
ProductType.Twins -> AppScreen.OnboardingTwins
|
||||
ProductType.SaltPay -> AppScreen.OnboardingWallet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.features.onboarding
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class OnboardingMenuProvider : MenuProvider {
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.menu_onboarding, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.menu_item_chat_support -> {
|
||||
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.tap.features.onboarding
|
||||
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.extensions.successOr
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayActivationStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.updateActivationStatus
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class OnboardingSaltPayHelper {
|
||||
companion object {
|
||||
|
||||
suspend fun isOnboardingCase(
|
||||
scanResponse: ScanResponse,
|
||||
manager: SaltPayActivationManager,
|
||||
): Result<Boolean> {
|
||||
return try {
|
||||
val status = manager.updateActivationStatus(
|
||||
amountToClaim = null,
|
||||
step = SaltPayActivationStep.None,
|
||||
).successOr { return it }
|
||||
|
||||
val isRegistrationCase = status.step != SaltPayActivationStep.Finished
|
||||
val isBackupCase = scanResponse.card.backupStatus?.isActive == false
|
||||
Result.Success(isRegistrationCase || isBackupCase)
|
||||
} catch (ex: Exception) {
|
||||
Result.Failure(ex)
|
||||
} catch (error: SaltPayActivationError) {
|
||||
Result.Failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun testProceedToOnboarding(
|
||||
scanResponse: ScanResponse,
|
||||
manager: SaltPayActivationManager,
|
||||
): Result<Boolean> = Result.Success(true)
|
||||
|
||||
suspend fun testProceedToMainScreen(
|
||||
scanResponse: ScanResponse,
|
||||
manager: SaltPayActivationManager,
|
||||
): Result<Boolean> = Result.Success(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,14 @@
|
|||
package com.tangem.tap.features.onboarding.products
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.MenuProvider
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.transitions.HomeToOnboardingTransition
|
||||
import com.tangem.tap.features.BaseStoreFragment
|
||||
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentOnboardingMainBinding
|
||||
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
|
||||
|
|
@ -22,6 +27,12 @@ abstract class BaseOnboardingFragment<T> : BaseStoreFragment(R.layout.fragment_o
|
|||
exitTransition = HomeToOnboardingTransition()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
||||
}
|
||||
|
||||
override fun loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider()
|
||||
|
||||
protected fun showConfetti(show: Boolean) = with(binding.vConfetti) {
|
||||
lavConfetti.show(show)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
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 coil.load
|
||||
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.redux.navigation.ShareElement
|
||||
|
|
@ -33,23 +28,18 @@ import com.tangem.wallet.R
|
|||
class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
||||
|
||||
private var previousStep: OnboardingNoteStep = OnboardingNoteStep.None
|
||||
private val mainBinding by lazy { binding.vMain }
|
||||
|
||||
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
postponeEnterTransition()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
addBackPressHandler(this)
|
||||
|
||||
binding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard
|
||||
startPostponedEnterTransition()
|
||||
mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard
|
||||
|
||||
binding.toolbar.setTitle(R.string.onboarding_title)
|
||||
btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
|
||||
btnRefreshBalanceWidget = RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer)
|
||||
|
||||
store.dispatch(OnboardingNoteAction.LoadCardArtwork)
|
||||
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
|
||||
|
|
@ -67,7 +57,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
override fun newState(state: OnboardingNoteState) {
|
||||
if (activity == null || view == null) return
|
||||
|
||||
binding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
|
||||
mainBinding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
|
||||
placeholder(R.drawable.card_placeholder_black)
|
||||
error(R.drawable.card_placeholder_black)
|
||||
fallback(R.drawable.card_placeholder_black)
|
||||
|
|
@ -90,7 +80,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
private fun setBalance(state: OnboardingNoteState) {
|
||||
if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return
|
||||
|
||||
with (binding.onboardingTopContainer.onboardingTvBalance) {
|
||||
with(mainBinding.onboardingTopContainer.onboardingTvBalance) {
|
||||
if (state.balanceCriticalError == null) {
|
||||
val balanceValue = state.walletBalance.value.stripZeroPlainString()
|
||||
tvBalanceValue.text = balanceValue
|
||||
|
|
@ -102,7 +92,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupNoneState() = with(binding.onboardingActionContainer) {
|
||||
private fun setupNoneState() = with(mainBinding.onboardingActionContainer) {
|
||||
btnMainAction.isVisible = false
|
||||
btnAlternativeAction.isVisible = false
|
||||
btnRefreshBalanceWidget.show(false)
|
||||
|
|
@ -116,7 +106,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
}
|
||||
|
||||
private fun setupCreateWalletState(state: OnboardingNoteState) =
|
||||
with(binding.onboardingActionContainer) {
|
||||
with(mainBinding.onboardingActionContainer) {
|
||||
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
|
||||
btnMainAction.setOnClickListener { store.dispatch(OnboardingNoteAction.CreateWallet) }
|
||||
btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean)
|
||||
|
|
@ -127,15 +117,15 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
tvHeader.setText(R.string.onboarding_create_wallet_header)
|
||||
tvBody.setText(R.string.onboarding_create_wallet_body)
|
||||
|
||||
binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
|
||||
requireContext().getDrawableCompat(R.drawable.shape_circle)
|
||||
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
|
||||
requireContext().getDrawableCompat(R.drawable.shape_circle),
|
||||
)
|
||||
updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet)
|
||||
|
||||
btnAlternativeAction.isVisible = false // temporary
|
||||
}
|
||||
|
||||
private fun setupTopUpWalletState(state: OnboardingNoteState) = with(binding.onboardingActionContainer) {
|
||||
private fun setupTopUpWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
|
||||
if (state.isBuyAllowed) {
|
||||
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
|
||||
btnMainAction.setOnClickListener {
|
||||
|
|
@ -163,7 +153,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
state.walletBalance.amountToCreateAccount?.let { amount ->
|
||||
val tvBodyMessage = getString(
|
||||
R.string.onboarding_top_up_body_no_account_error,
|
||||
amount, state.walletBalance.currency.currencySymbol
|
||||
amount, state.walletBalance.currency.currencySymbol,
|
||||
)
|
||||
tvBody.text = tvBodyMessage
|
||||
}
|
||||
|
|
@ -175,12 +165,13 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
store.dispatch(OnboardingNoteAction.Balance.Update)
|
||||
}
|
||||
}
|
||||
binding.onboardingTopContainer.imvCardBackground
|
||||
.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
|
||||
|
||||
val drawable = requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)
|
||||
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(drawable)
|
||||
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet)
|
||||
}
|
||||
|
||||
private fun setupDoneState(state: OnboardingNoteState) = with(binding.onboardingActionContainer) {
|
||||
private fun setupDoneState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
|
||||
btnMainAction.setText(R.string.onboarding_done_button_continue)
|
||||
btnMainAction.setOnClickListener {
|
||||
showConfetti(false)
|
||||
|
|
@ -188,15 +179,15 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
}
|
||||
|
||||
btnAlternativeAction.isVisible = false
|
||||
btnAlternativeAction.setText("")
|
||||
btnAlternativeAction.text = ""
|
||||
btnAlternativeAction.setOnClickListener { }
|
||||
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
|
||||
|
||||
tvHeader.setText(R.string.onboarding_done_header)
|
||||
tvBody.setText(R.string.onboarding_done_body)
|
||||
|
||||
binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
|
||||
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)
|
||||
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
|
||||
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8),
|
||||
)
|
||||
updateConstraints(state.currentStep, R.layout.lp_onboarding_done_activation)
|
||||
}
|
||||
|
|
@ -204,22 +195,13 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
private fun updateConstraints(currentStep: OnboardingNoteStep, @LayoutRes layoutId: Int) {
|
||||
if (this.previousStep == currentStep) return
|
||||
|
||||
with(binding.onboardingTopContainer) {
|
||||
with(mainBinding.onboardingTopContainer) {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(requireContext(), layoutId)
|
||||
constraintSet.applyTo(onboardingMainContainer)
|
||||
constraintSet.applyTo(onboardingWalletContainer)
|
||||
val transition = InternalNoteLayoutTransition()
|
||||
transition.interpolator = OvershootInterpolator()
|
||||
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
|
||||
TransitionManager.beginDelayedTransition(onboardingWalletContainer, 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,12 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.getAddressData
|
||||
import com.tangem.tap.common.extensions.getToUpUrl
|
||||
import com.tangem.tap.common.extensions.onCardScanned
|
||||
import com.tangem.tap.common.postUi
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
|
|
@ -115,11 +120,11 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
|
|||
val balanceIsLoading = noteState.walletBalance.copy(
|
||||
currency = Currency.Blockchain(
|
||||
walletManager.wallet.blockchain,
|
||||
walletManager.wallet.publicKey.derivationPath?.rawPath
|
||||
walletManager.wallet.publicKey.derivationPath?.rawPath,
|
||||
),
|
||||
state = ProgressState.Loading,
|
||||
error = null,
|
||||
criticalError = null
|
||||
criticalError = null,
|
||||
)
|
||||
store.dispatch(OnboardingNoteAction.Balance.Set(balanceIsLoading))
|
||||
|
||||
|
|
|
|||
|
|
@ -24,17 +24,13 @@ import com.tangem.wallet.R
|
|||
*/
|
||||
class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCardsState>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
postponeEnterTransition()
|
||||
}
|
||||
private val mainBinding by lazy { binding.vMain }
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
addBackPressHandler(this)
|
||||
|
||||
binding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard
|
||||
startPostponedEnterTransition()
|
||||
mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard
|
||||
|
||||
binding.toolbar.setTitle(R.string.onboarding_title)
|
||||
store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork)
|
||||
|
|
@ -54,7 +50,7 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
|
|||
if (activity == null || view == null) return
|
||||
if (state.currentStep == OnboardingOtherCardsStep.None) return
|
||||
|
||||
binding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
|
||||
mainBinding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
|
||||
placeholder(R.drawable.card_placeholder_black)
|
||||
error(R.drawable.card_placeholder_black)
|
||||
fallback(R.drawable.card_placeholder_black)
|
||||
|
|
@ -70,29 +66,25 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
|
|||
showConfetti(state.showConfetti)
|
||||
}
|
||||
|
||||
private fun setupCreateWalletState() = with(binding) {
|
||||
private fun setupCreateWalletState() = with(mainBinding.onboardingActionContainer) {
|
||||
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
|
||||
btnMainAction.setOnClickListener { store.dispatch(OnboardingOtherCardsAction.CreateWallet) }
|
||||
btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean)
|
||||
btnAlternativeAction.setOnClickListener { }
|
||||
|
||||
with(onboardingActionContainer) {
|
||||
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
|
||||
btnMainAction.setOnClickListener { store.dispatch(OnboardingOtherCardsAction.CreateWallet) }
|
||||
btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean)
|
||||
btnAlternativeAction.setOnClickListener { }
|
||||
tvHeader.setText(R.string.onboarding_create_wallet_header)
|
||||
tvBody.setText(R.string.onboarding_create_wallet_body)
|
||||
|
||||
tvHeader.setText(R.string.onboarding_create_wallet_header)
|
||||
tvBody.setText(R.string.onboarding_create_wallet_body)
|
||||
btnAlternativeAction.isVisible = false // temporary
|
||||
|
||||
btnAlternativeAction.isVisible = false // temporary
|
||||
}
|
||||
|
||||
|
||||
binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
|
||||
requireContext().getDrawableCompat(R.drawable.shape_circle)
|
||||
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
|
||||
requireContext().getDrawableCompat(R.drawable.shape_circle),
|
||||
)
|
||||
updateConstraints(R.layout.lp_onboarding_create_wallet)
|
||||
|
||||
}
|
||||
|
||||
private fun setupDoneState() = with(binding.onboardingActionContainer) {
|
||||
private fun setupDoneState() = with(mainBinding.onboardingActionContainer) {
|
||||
btnMainAction.setText(R.string.onboarding_done_button_continue)
|
||||
btnMainAction.setOnClickListener {
|
||||
showConfetti(false)
|
||||
|
|
@ -106,20 +98,18 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
|
|||
tvHeader.setText(R.string.onboarding_done_header)
|
||||
tvBody.setText(R.string.onboarding_done_body)
|
||||
|
||||
binding.onboardingTopContainer.imvCardBackground?.setBackgroundDrawable(
|
||||
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)
|
||||
mainBinding.onboardingTopContainer.imvCardBackground?.setBackgroundDrawable(
|
||||
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8),
|
||||
)
|
||||
updateConstraints(R.layout.lp_onboarding_done)
|
||||
}
|
||||
|
||||
private fun updateConstraints(@LayoutRes layoutId: Int) {
|
||||
with(binding.onboardingTopContainer) {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(requireContext(), layoutId)
|
||||
constraintSet.applyTo(onboardingMainContainer)
|
||||
val transition = InternalNoteLayoutTransition()
|
||||
transition.interpolator = OvershootInterpolator()
|
||||
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
|
||||
}
|
||||
private fun updateConstraints(@LayoutRes layoutId: Int) = with(mainBinding.onboardingTopContainer) {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(requireContext(), layoutId)
|
||||
constraintSet.applyTo(onboardingWalletContainer)
|
||||
val transition = InternalNoteLayoutTransition()
|
||||
transition.interpolator = OvershootInterpolator()
|
||||
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
|
||||
}
|
||||
}
|
||||
|
|
@ -79,17 +79,21 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
|
|||
val primaryBlockchain = updatedResponse.getBlockchain()
|
||||
val blockchainNetworks = if (primaryBlockchain != Blockchain.Unknown) {
|
||||
val primaryToken = updatedResponse.getPrimaryToken()
|
||||
val blockchainNetwork = BlockchainNetwork(primaryBlockchain, updatedResponse.card).updateTokens(
|
||||
listOfNotNull(primaryToken))
|
||||
val blockchainNetwork =
|
||||
BlockchainNetwork(primaryBlockchain, updatedResponse.card).updateTokens(
|
||||
listOfNotNull(primaryToken),
|
||||
)
|
||||
listOf(blockchainNetwork)
|
||||
} else {
|
||||
listOf(
|
||||
BlockchainNetwork(Blockchain.Bitcoin, updatedResponse.card),
|
||||
BlockchainNetwork(Blockchain.Ethereum, updatedResponse.card)
|
||||
BlockchainNetwork(Blockchain.Ethereum, updatedResponse.card),
|
||||
)
|
||||
}
|
||||
|
||||
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks))
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks, updatedResponse.card),
|
||||
)
|
||||
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
|
|||
|
||||
class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
||||
|
||||
private val mainBinding by lazy { binding.vMain }
|
||||
private var previousStep = TwinCardsStep.None
|
||||
|
||||
private lateinit var twinsWidget: TwinsCardWidget
|
||||
|
|
@ -47,7 +48,6 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
postponeEnterTransition()
|
||||
store.dispatch(TwinCardsAction.Init)
|
||||
}
|
||||
|
||||
|
|
@ -66,30 +66,30 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
addBackPressHandler(this)
|
||||
|
||||
reconfigureLayoutForTwins(binding.onboardingTopContainer)
|
||||
reconfigureLayoutForTwins(mainBinding.onboardingTopContainer)
|
||||
|
||||
val typedValue = TypedValue()
|
||||
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
|
||||
val deviceScaleFactorForWelcomeState = typedValue.float
|
||||
|
||||
twinsWidget = TwinsCardWidget(
|
||||
LeapfrogWidget(binding.onboardingTopContainer.cardsContainer),
|
||||
deviceScaleFactorForWelcomeState
|
||||
LeapfrogWidget(mainBinding.onboardingTopContainer.cardsContainer),
|
||||
deviceScaleFactorForWelcomeState,
|
||||
) {
|
||||
285f * deviceScaleFactorForWelcomeState
|
||||
}
|
||||
btnRefreshBalanceWidget =
|
||||
RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
|
||||
RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer)
|
||||
|
||||
binding.toolbar.title = getText(R.string.twins_recreate_toolbar)
|
||||
|
||||
binding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1) {
|
||||
mainBinding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1) {
|
||||
placeholder(R.drawable.card_placeholder_black)
|
||||
error(R.drawable.card_placeholder_black)
|
||||
fallback(R.drawable.card_placeholder_black)
|
||||
}
|
||||
|
||||
binding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2) {
|
||||
mainBinding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2) {
|
||||
placeholder(R.drawable.card_placeholder_white)
|
||||
error(R.drawable.card_placeholder_white)
|
||||
fallback(R.drawable.card_placeholder_white)
|
||||
|
|
@ -143,7 +143,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
private fun setBalance(state: TwinCardsState) {
|
||||
if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return
|
||||
|
||||
with(binding.onboardingTopContainer.onboardingTvBalance) {
|
||||
with(mainBinding.onboardingTopContainer.onboardingTvBalance) {
|
||||
if (state.balanceCriticalError == null) {
|
||||
val balanceValue = state.walletBalance.value.stripZeroPlainString()
|
||||
tvBalanceValue.text = balanceValue
|
||||
|
|
@ -167,26 +167,26 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
}
|
||||
|
||||
private fun setupWelcomeState(state: TwinCardsState, mainAction: VoidCallback) =
|
||||
with(binding.onboardingActionContainer) {
|
||||
twinsWidget.toWelcome(false) { startPostponedEnterTransition() }
|
||||
with(mainBinding.onboardingActionContainer) {
|
||||
twinsWidget.toWelcome(false)
|
||||
|
||||
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.show()
|
||||
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.show()
|
||||
pbBinding.pbState.hide()
|
||||
|
||||
tvHeader.setText(R.string.twins_onboarding_subtitle)
|
||||
tvBody.text = getString(
|
||||
R.string.twins_onboarding_description_format,
|
||||
state.cardNumber?.pairIndexNumber()
|
||||
state.cardNumber?.pairIndexNumber(),
|
||||
)
|
||||
|
||||
btnMainAction.setText(R.string.common_continue)
|
||||
btnMainAction.setOnClickListener { mainAction() }
|
||||
}
|
||||
|
||||
private fun setupWarningState(state: TwinCardsState) = with(binding.onboardingActionContainer) {
|
||||
twinsWidget.toWelcome(false) { startPostponedEnterTransition() }
|
||||
private fun setupWarningState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
|
||||
twinsWidget.toWelcome(false)
|
||||
|
||||
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide()
|
||||
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide()
|
||||
pbBinding.pbState.hide()
|
||||
chbUnderstand.show()
|
||||
|
||||
|
|
@ -203,198 +203,185 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupCreateFirstWalletState(state: TwinCardsState) =
|
||||
with(binding.onboardingActionContainer) {
|
||||
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide()
|
||||
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleLarge.hide()
|
||||
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMedium.hide()
|
||||
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMin.hide()
|
||||
chbUnderstand.hide()
|
||||
pbBinding.pbState.show()
|
||||
private fun setupCreateFirstWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
|
||||
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide()
|
||||
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleLarge.hide()
|
||||
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMedium.hide()
|
||||
mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMin.hide()
|
||||
chbUnderstand.hide()
|
||||
pbBinding.pbState.show()
|
||||
|
||||
binding.onboardingTopContainer.onboardingMainContainer.beginDelayedTransition()
|
||||
mainBinding.onboardingTopContainer.onboardingWalletContainer.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()
|
||||
tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
|
||||
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
|
||||
|
||||
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
|
||||
btnMainAction.setOnClickListener {
|
||||
store.dispatch(
|
||||
TwinCardsAction.Wallet.LaunchFirstStep(
|
||||
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
|
||||
AndroidAssetReader(requireContext()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
btnAlternativeAction.isVisible = false
|
||||
btnAlternativeAction.isClickable = false
|
||||
}
|
||||
|
||||
private fun setupCreateSecondWalletState(state: TwinCardsState) =
|
||||
with(binding.onboardingActionContainer) {
|
||||
switchToCard(state.cardNumber?.pairNumber())
|
||||
|
||||
val twinPairIndexNumber = state.cardNumber?.pairIndexNumber()
|
||||
tvHeader.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber)
|
||||
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
|
||||
|
||||
btnMainAction.text =
|
||||
getString(R.string.twins_recreate_button_format, twinPairIndexNumber)
|
||||
btnMainAction.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) =
|
||||
with(binding.onboardingActionContainer) {
|
||||
switchToCard(state.cardNumber)
|
||||
|
||||
val twinIndexNumber = state.cardNumber?.indexNumber()
|
||||
tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
|
||||
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
|
||||
|
||||
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
|
||||
btnMainAction.setOnClickListener {
|
||||
store.dispatch(
|
||||
TwinCardsAction.Wallet.LaunchThirdStep(
|
||||
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun setupTopUpWalletState(state: TwinCardsState) =
|
||||
with(binding.onboardingActionContainer) {
|
||||
when (previousStep) {
|
||||
TwinCardsStep.None -> {
|
||||
when (previousStep) {
|
||||
TwinCardsStep.None -> {
|
||||
twinsWidget.toLeapfrog(false) {
|
||||
when (state.cardNumber) {
|
||||
TwinCardNumber.First -> {
|
||||
twinsWidget.leapfrogWidget.unfold(false) {
|
||||
twinsWidget.toActivate(false) {
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
TwinCardNumber.First -> {}
|
||||
TwinCardNumber.Second -> {
|
||||
switchToCard(state.cardNumber, false) {
|
||||
twinsWidget.toActivate(false)
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
switchToCard(
|
||||
state.cardNumber,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
TwinCardsStep.CreateThirdWallet -> {
|
||||
twinsWidget.toActivate()
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isBuyAllowed) {
|
||||
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
|
||||
btnMainAction.setOnClickListener {
|
||||
store.dispatch(TwinCardsAction.TopUp)
|
||||
}
|
||||
|
||||
btnAlternativeAction.isVisible = true
|
||||
btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address)
|
||||
btnAlternativeAction.setOnClickListener {
|
||||
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
|
||||
}
|
||||
} else {
|
||||
btnMainAction.setText(R.string.onboarding_button_receive_crypto)
|
||||
btnMainAction.setOnClickListener {
|
||||
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
|
||||
}
|
||||
|
||||
btnAlternativeAction.isVisible = false
|
||||
TwinCardsStep.Welcome, TwinCardsStep.Warning -> {
|
||||
twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) })
|
||||
}
|
||||
|
||||
tvHeader.setText(R.string.onboarding_top_up_header)
|
||||
tvBody.setText(R.string.onboarding_top_up_body)
|
||||
|
||||
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
|
||||
if (btnRefreshBalanceWidget.isShowing != true) {
|
||||
btnRefreshBalanceWidget.mainView.setOnClickListener {
|
||||
store.dispatch(TwinCardsAction.Balance.Update)
|
||||
}
|
||||
}
|
||||
binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
|
||||
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)
|
||||
)
|
||||
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins)
|
||||
}
|
||||
|
||||
private fun setupDoneState(state: TwinCardsState) =
|
||||
with(binding.onboardingActionContainer) {
|
||||
btnMainAction.setText(R.string.onboarding_done_button_continue)
|
||||
val twinIndexNumber = state.cardNumber?.indexNumber()
|
||||
tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
|
||||
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
|
||||
|
||||
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
|
||||
btnMainAction.setOnClickListener {
|
||||
store.dispatch(
|
||||
TwinCardsAction.Wallet.LaunchFirstStep(
|
||||
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
|
||||
AndroidAssetReader(requireContext()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
btnAlternativeAction.isVisible = false
|
||||
btnAlternativeAction.isClickable = false
|
||||
}
|
||||
|
||||
private fun setupCreateSecondWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
|
||||
switchToCard(state.cardNumber?.pairNumber())
|
||||
|
||||
val twinPairIndexNumber = state.cardNumber?.pairIndexNumber()
|
||||
tvHeader.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber)
|
||||
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
|
||||
|
||||
btnMainAction.text =
|
||||
getString(R.string.twins_recreate_button_format, twinPairIndexNumber)
|
||||
btnMainAction.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) = with(mainBinding.onboardingActionContainer) {
|
||||
switchToCard(state.cardNumber)
|
||||
|
||||
val twinIndexNumber = state.cardNumber?.indexNumber()
|
||||
tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
|
||||
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
|
||||
|
||||
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
|
||||
btnMainAction.setOnClickListener {
|
||||
store.dispatch(
|
||||
TwinCardsAction.Wallet.LaunchThirdStep(
|
||||
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTopUpWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
|
||||
when (previousStep) {
|
||||
TwinCardsStep.None -> {
|
||||
when (state.cardNumber) {
|
||||
TwinCardNumber.First -> {
|
||||
twinsWidget.leapfrogWidget.unfold(false) {
|
||||
twinsWidget.toActivate(false)
|
||||
}
|
||||
}
|
||||
TwinCardNumber.Second -> {
|
||||
switchToCard(state.cardNumber, false) {
|
||||
twinsWidget.toActivate(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TwinCardsStep.CreateThirdWallet -> {
|
||||
twinsWidget.toActivate()
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isBuyAllowed) {
|
||||
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
|
||||
btnMainAction.setOnClickListener {
|
||||
store.dispatch(TwinCardsAction.Confetti.Hide)
|
||||
store.dispatch(TwinCardsAction.Done)
|
||||
store.dispatch(TwinCardsAction.TopUp)
|
||||
}
|
||||
|
||||
btnAlternativeAction.isVisible = true
|
||||
btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address)
|
||||
btnAlternativeAction.setOnClickListener {
|
||||
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
|
||||
}
|
||||
} else {
|
||||
btnMainAction.setText(R.string.onboarding_button_receive_crypto)
|
||||
btnMainAction.setOnClickListener {
|
||||
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
|
||||
}
|
||||
|
||||
btnAlternativeAction.isVisible = false
|
||||
btnAlternativeAction.isClickable = false
|
||||
|
||||
tvHeader.setText(R.string.onboarding_done_header)
|
||||
tvBody.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)
|
||||
}
|
||||
|
||||
tvHeader.setText(R.string.onboarding_top_up_header)
|
||||
tvBody.setText(R.string.onboarding_top_up_body)
|
||||
|
||||
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
|
||||
if (btnRefreshBalanceWidget.isShowing != true) {
|
||||
btnRefreshBalanceWidget.mainView.setOnClickListener {
|
||||
store.dispatch(TwinCardsAction.Balance.Update)
|
||||
}
|
||||
}
|
||||
mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
|
||||
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8),
|
||||
)
|
||||
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins)
|
||||
}
|
||||
|
||||
private fun setupDoneState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) {
|
||||
btnMainAction.setText(R.string.onboarding_done_button_continue)
|
||||
btnMainAction.setOnClickListener {
|
||||
store.dispatch(TwinCardsAction.Confetti.Hide)
|
||||
store.dispatch(TwinCardsAction.Done)
|
||||
}
|
||||
|
||||
btnAlternativeAction.isVisible = false
|
||||
btnAlternativeAction.isClickable = false
|
||||
|
||||
tvHeader.setText(R.string.onboarding_done_header)
|
||||
tvBody.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
|
||||
|
||||
with(binding.onboardingTopContainer) {
|
||||
with(mainBinding.onboardingTopContainer) {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(requireContext(), layoutId)
|
||||
constraintSet.applyTo(onboardingMainContainer)
|
||||
constraintSet.applyTo(onboardingWalletContainer)
|
||||
val transition = InternalNoteLayoutTransition()
|
||||
transition.interpolator = OvershootInterpolator()
|
||||
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
|
||||
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchToCard(
|
||||
cardNumber: TwinCardNumber?,
|
||||
animate: Boolean = true,
|
||||
onEnd: VoidCallback = {}
|
||||
) {
|
||||
private fun switchToCard(cardNumber: TwinCardNumber?, animate: Boolean = true, onEnd: VoidCallback = {}) {
|
||||
val position = cardNumber?.number ?: return
|
||||
|
||||
if (twinsWidget.leapfrogWidget.getViewPositionByIndex(1) != position - 1) {
|
||||
|
|
@ -405,12 +392,14 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
}
|
||||
|
||||
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()
|
||||
})
|
||||
store.dispatch(
|
||||
TwinCardsAction.Wallet.HandleOnBackPressed { should, popAction ->
|
||||
store.dispatch(TwinCardsAction.Confetti.Hide)
|
||||
showConfetti(false)
|
||||
if (should) switchToCard(TwinCardNumber.First, true, popAction)
|
||||
else popAction()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import org.rekotlin.Action
|
|||
|
||||
sealed class OnboardingWalletAction : Action {
|
||||
object Init : OnboardingWalletAction()
|
||||
object StartSaltPayCardActivation : OnboardingWalletAction()
|
||||
object GetToCreateWalletStep : OnboardingWalletAction()
|
||||
object GetToSaltPayStep : OnboardingWalletAction()
|
||||
object CreateWallet : OnboardingWalletAction()
|
||||
object Done : OnboardingWalletAction()
|
||||
object FinishOnboarding : OnboardingWalletAction()
|
||||
|
||||
object ProceedBackup : OnboardingWalletAction()
|
||||
object ResumeBackup : OnboardingWalletAction()
|
||||
|
||||
object LoadArtwork : OnboardingWalletAction()
|
||||
class SetArtworkUrl(val artworkUrl: String?) : OnboardingWalletAction()
|
||||
|
|
@ -28,6 +30,11 @@ sealed class BackupAction : Action {
|
|||
object StartAddingPrimaryCard : BackupAction()
|
||||
object ScanPrimaryCard : BackupAction()
|
||||
|
||||
/**
|
||||
* Check for unfinished backup of standard Wallet cards.
|
||||
* For SaltPay cards unfinished backup resumed after scanning the card on HomeScreen through Onboarding.Start.
|
||||
* See more Onboarding.Start, CheckForUnfinishedBackup, StartForUnfinishedBackup
|
||||
*/
|
||||
object CheckForUnfinishedBackup : BackupAction()
|
||||
|
||||
object StartAddingBackupCards : BackupAction()
|
||||
|
|
@ -53,15 +60,13 @@ sealed class BackupAction : Action {
|
|||
data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction()
|
||||
object OnAccessCodeDialogClosed : BackupAction()
|
||||
|
||||
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
|
||||
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
|
||||
|
||||
object PrepareToWritePrimaryCard : BackupAction()
|
||||
object WritePrimaryCard : BackupAction()
|
||||
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
|
||||
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
|
||||
|
||||
object FinishBackup : BackupAction()
|
||||
object DiscardBackup : BackupAction()
|
||||
object DiscardSavedBackup : BackupAction()
|
||||
object ResumeBackup : BackupAction()
|
||||
|
||||
object ResumeFoundUnfinishedBackup : BackupAction()
|
||||
}
|
||||
|
|
@ -3,10 +3,13 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.extensions.ifNotNull
|
||||
import com.tangem.domain.common.SaltPayWorkaround
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.backupService
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -16,10 +19,19 @@ import com.tangem.tap.domain.extensions.hasWallets
|
|||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
import com.tangem.tap.features.wallet.redux.Artwork
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdk
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
class OnboardingWalletMiddleware {
|
||||
|
|
@ -31,35 +43,59 @@ class OnboardingWalletMiddleware {
|
|||
private val onboardingWalletMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
handleWalletAction(action)
|
||||
handleWalletAction(action, state, dispatch)
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleWalletAction(action: Action) {
|
||||
private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: DispatchFunction) {
|
||||
if (action !is OnboardingWalletAction) return
|
||||
|
||||
val globalState = store.state.globalState
|
||||
val onboardingManager = globalState.onboardingState.onboardingManager
|
||||
|
||||
val scanResponse = onboardingManager?.scanResponse
|
||||
val card = scanResponse?.card
|
||||
|
||||
val walletState = store.state.onboardingWalletState
|
||||
val onboardingWalletState = store.state.onboardingWalletState
|
||||
|
||||
when (action) {
|
||||
OnboardingWalletAction.Init -> {
|
||||
val action = when {
|
||||
when {
|
||||
card == null -> {
|
||||
OnboardingWalletAction.ProceedBackup
|
||||
// it's possible when found unfinished backup for standard Wallet cards
|
||||
store.dispatch(OnboardingWalletAction.ResumeBackup)
|
||||
}
|
||||
card.hasWallets() && card.backupStatus == Card.BackupStatus.NoBackup -> {
|
||||
store.dispatch(OnboardingWalletAction.ResumeBackup)
|
||||
}
|
||||
card.hasWallets() && card.backupStatus?.isActive == true -> {
|
||||
when {
|
||||
// check for unfinished backup for saltPay cards. See more
|
||||
scanResponse.isSaltPay() && backupService.hasIncompletedBackup -> {
|
||||
store.dispatch(OnboardingWalletAction.ResumeBackup)
|
||||
}
|
||||
scanResponse.isSaltPay() -> {
|
||||
store.dispatch(OnboardingWalletAction.GetToSaltPayStep)
|
||||
store.dispatch(BackupAction.FinishBackup)
|
||||
}
|
||||
else -> store.dispatch(BackupAction.FinishBackup)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
store.dispatch(OnboardingWalletAction.GetToCreateWalletStep)
|
||||
}
|
||||
}
|
||||
}
|
||||
is OnboardingWalletAction.StartSaltPayCardActivation -> {
|
||||
// we need to do this because the standard activation happens in the OnboardingWalletAction.CreateWallet
|
||||
// step which is omitted for SaltPay cards
|
||||
if (onboardingWalletState.isSaltPay) {
|
||||
ifNotNull(onboardingManager, scanResponse?.card?.cardId) { manager, cardId ->
|
||||
manager.activationStarted(cardId)
|
||||
}
|
||||
card.hasWallets() && card.backupStatus == Card.BackupStatus.NoBackup ->
|
||||
OnboardingWalletAction.ProceedBackup
|
||||
card.hasWallets() && card.backupStatus?.isActive == true ->
|
||||
BackupAction.FinishBackup
|
||||
else -> OnboardingWalletAction.GetToCreateWalletStep
|
||||
}
|
||||
store.dispatch(action)
|
||||
}
|
||||
is OnboardingWalletAction.LoadArtwork -> {
|
||||
scope.launch {
|
||||
|
|
@ -80,25 +116,24 @@ private fun handleWalletAction(action: Action) {
|
|||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
//here we must use updated scanResponse after createWallet & derivation
|
||||
val updatedResponse =
|
||||
globalState.onboardingState.onboardingManager.scanResponse.copy(
|
||||
card = result.data.card,
|
||||
derivedKeys = result.data.derivedKeys,
|
||||
primaryCard = result.data.primaryCard
|
||||
)
|
||||
val updatedResponse = globalState.onboardingState.onboardingManager.scanResponse.copy(
|
||||
card = result.data.card,
|
||||
derivedKeys = result.data.derivedKeys,
|
||||
primaryCard = result.data.primaryCard,
|
||||
)
|
||||
onboardingManager.scanResponse = updatedResponse
|
||||
val blockchainNetworks = listOf(
|
||||
BlockchainNetwork(Blockchain.Bitcoin, result.data.card),
|
||||
BlockchainNetwork(Blockchain.Ethereum, result.data.card)
|
||||
BlockchainNetwork(Blockchain.Ethereum, result.data.card),
|
||||
)
|
||||
store.dispatchOnMain(
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.SaveCurrencies(
|
||||
blockchainNetworks = blockchainNetworks,
|
||||
card = result.data.card,
|
||||
),
|
||||
)
|
||||
onboardingManager.activationStarted(updatedResponse.card.cardId)
|
||||
store.dispatch(OnboardingWalletAction.ProceedBackup)
|
||||
store.dispatch(OnboardingWalletAction.ResumeBackup)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
// do nothing
|
||||
|
|
@ -110,6 +145,7 @@ private fun handleWalletAction(action: Action) {
|
|||
OnboardingWalletAction.FinishOnboarding -> {
|
||||
store.dispatch(GlobalAction.Onboarding.Stop)
|
||||
|
||||
//TODO: SaltPay: scanResponse
|
||||
if (scanResponse == null) {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
store.dispatch(HomeAction.ReadCard)
|
||||
|
|
@ -120,15 +156,19 @@ private fun handleWalletAction(action: Action) {
|
|||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
}
|
||||
}
|
||||
OnboardingWalletAction.ProceedBackup -> {
|
||||
is OnboardingWalletAction.ResumeBackup -> {
|
||||
val newAction = when (val backupState = backupService.currentState) {
|
||||
BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
|
||||
is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index)
|
||||
else -> {
|
||||
if (walletState.backupState.backupStep == BackupStep.InitBackup ||
|
||||
walletState.backupState.backupStep == BackupStep.Finished
|
||||
if (onboardingWalletState.backupState.backupStep == BackupStep.InitBackup ||
|
||||
onboardingWalletState.backupState.backupStep == BackupStep.Finished
|
||||
) {
|
||||
BackupAction.IntroduceBackup
|
||||
if (onboardingWalletState.isSaltPay) {
|
||||
BackupAction.StartBackup
|
||||
} else {
|
||||
BackupAction.IntroduceBackup
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -137,16 +177,9 @@ private fun handleWalletAction(action: Action) {
|
|||
newAction?.let { store.dispatch(it) }
|
||||
}
|
||||
OnboardingWalletAction.OnBackPressed -> {
|
||||
when (walletState.backupState.backupStep) {
|
||||
BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo())
|
||||
BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,
|
||||
BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard,
|
||||
-> {
|
||||
store.dispatch(BackupAction.DiscardBackup)
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
is BackupStep.WriteBackupCard ->
|
||||
store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress))
|
||||
when {
|
||||
onboardingWalletState.isSaltPay -> handleOnBackPressedSaltPay(onboardingWalletState)
|
||||
else -> handleOnBackPressed(onboardingWalletState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -159,7 +192,7 @@ private fun updateScanResponseAfterBackup(
|
|||
val cardsCount = backupState.backupCardsNumber
|
||||
scanResponse.card.copy(
|
||||
backupStatus = Card.BackupStatus.Active(cardCount = cardsCount),
|
||||
isAccessCodeSet = true
|
||||
isAccessCodeSet = true,
|
||||
)
|
||||
} else {
|
||||
scanResponse.card
|
||||
|
|
@ -180,13 +213,11 @@ class BackupMiddleware {
|
|||
|
||||
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
|
||||
if (DemoHelper.tryHandle(appState, action)) return
|
||||
val globalState = appState()?.globalState ?: return
|
||||
val onboardingWalletState = appState()?.onboardingWalletState ?: return
|
||||
|
||||
val backupState = store.state.onboardingWalletState.backupState
|
||||
|
||||
val globalState = store.state.globalState
|
||||
val onboardingManager = globalState.onboardingState.onboardingManager
|
||||
|
||||
val scanResponse = onboardingManager?.scanResponse
|
||||
val backupState = onboardingWalletState.backupState
|
||||
val scanResponse = globalState.onboardingState.onboardingManager?.scanResponse
|
||||
val card = scanResponse?.card
|
||||
|
||||
when (action) {
|
||||
|
|
@ -212,32 +243,33 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
}
|
||||
}
|
||||
is BackupAction.AddBackupCard -> {
|
||||
onboardingWalletState.backupCardIdFilter?.let {
|
||||
backupService.skipCompatibilityChecks = true
|
||||
tangemSdk.config.filter.cardIdFilter = it
|
||||
}
|
||||
|
||||
backupService.addBackupCard { result ->
|
||||
backupService.skipCompatibilityChecks = false
|
||||
tangemSdk.config.filter.cardIdFilter = null
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
store.dispatchOnMain(BackupAction.AddBackupCard.Success)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is BackupAction.FinishAddingBackupCards -> {
|
||||
if (backupService.addedBackupCardsCount == 1) {
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards))
|
||||
}
|
||||
if (backupService.addedBackupCardsCount == 2) {
|
||||
if (backupService.addedBackupCardsCount == backupState.maxBackupCards) {
|
||||
store.dispatchOnMain(BackupAction.ShowAccessCodeInfoScreen)
|
||||
} else {
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards))
|
||||
}
|
||||
}
|
||||
is BackupAction.CheckAccessCode -> {
|
||||
if (action.accessCode.length < 4) {
|
||||
store.dispatch(
|
||||
BackupAction.SetAccessCodeError(
|
||||
AccessCodeError.CodeTooShort
|
||||
)
|
||||
)
|
||||
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodeTooShort))
|
||||
} else {
|
||||
store.dispatch(BackupAction.SetAccessCodeError(null))
|
||||
store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode))
|
||||
|
|
@ -247,22 +279,17 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
if (action.accessCodeConfirmation == backupState.accessCode) {
|
||||
store.dispatch(BackupAction.SetAccessCodeError(null))
|
||||
backupService.setAccessCode(action.accessCodeConfirmation)
|
||||
store.dispatch(OnboardingSaltPayAction.SetAccessCode(action.accessCodeConfirmation))
|
||||
store.dispatch(BackupAction.PrepareToWritePrimaryCard)
|
||||
} else {
|
||||
store.dispatch(
|
||||
BackupAction.SetAccessCodeError(
|
||||
AccessCodeError.CodesDoNotMatch
|
||||
)
|
||||
)
|
||||
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodesDoNotMatch))
|
||||
}
|
||||
}
|
||||
is BackupAction.WritePrimaryCard -> {
|
||||
backupService.proceedBackup { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
store.dispatchOnMain(
|
||||
BackupAction.PrepareToWriteBackupCard(1)
|
||||
)
|
||||
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(1))
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
}
|
||||
|
|
@ -274,15 +301,13 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
if (backupService.currentState == BackupService.State.Finished) {
|
||||
initSaltPayOnBackupFinishedIfNeeded(scanResponse, onboardingWalletState)
|
||||
store.dispatchOnMain(BackupAction.FinishBackup)
|
||||
} else {
|
||||
store.dispatchOnMain(
|
||||
BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1)
|
||||
)
|
||||
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1))
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -304,17 +329,68 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
backupService.discardSavedBackup()
|
||||
}
|
||||
is BackupAction.CheckForUnfinishedBackup -> {
|
||||
if (backupService.hasIncompletedBackup) {
|
||||
if (backupService.hasIncompletedBackup && !backupService.primaryCardIsSaltPay()) {
|
||||
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound))
|
||||
}
|
||||
}
|
||||
is BackupAction.ResumeBackup -> {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(null, true))
|
||||
store.dispatch(OnboardingWalletAction.ProceedBackup)
|
||||
is BackupAction.ResumeFoundUnfinishedBackup -> {
|
||||
store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup)
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
|
||||
}
|
||||
is BackupAction.DismissBackup -> {
|
||||
if (onboardingWalletState.isSaltPay) throw UnsupportedOperationException()
|
||||
store.dispatch(BackupAction.FinishBackup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupService.primaryCardIsSaltPay(): Boolean {
|
||||
return primaryCardId?.slice(0..3)?.let {
|
||||
SaltPayWorkaround.isVisaBatchId(it)
|
||||
} ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* SaltPay state maybe not initialized if backup resumed from ResumeFoundUnfinishedBackup action
|
||||
*/
|
||||
private fun initSaltPayOnBackupFinishedIfNeeded(
|
||||
scanResponse: ScanResponse?,
|
||||
onboardingWalletState: OnboardingWalletState,
|
||||
) {
|
||||
if (onboardingWalletState.isSaltPay && onboardingWalletState.onboardingSaltPayState == null) {
|
||||
//TODO: SaltPay: scanResponse
|
||||
if (scanResponse == null) throw IllegalArgumentException()
|
||||
|
||||
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
|
||||
store.dispatchOnMain(OnboardingSaltPayAction.Init.SetDependencies(manager, config))
|
||||
store.dispatchOnMain(OnboardingSaltPayAction.Update)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnBackPressedSaltPay(state: OnboardingWalletState) {
|
||||
when (state.backupState.backupStep) {
|
||||
BackupStep.Finished -> {
|
||||
store.dispatchDialogShow(
|
||||
SaltPayDialog.Activation.TryToInterrupt(
|
||||
onOk = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||
onCancel = { /* do nothing */ },
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> handleOnBackPressed(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnBackPressed(state: OnboardingWalletState) {
|
||||
when (state.backupState.backupStep) {
|
||||
BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo())
|
||||
BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,
|
||||
BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard,
|
||||
-> {
|
||||
store.dispatch(BackupAction.DiscardBackup)
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
is BackupStep.WriteBackupCard ->
|
||||
store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress))
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,13 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
|
|||
import com.tangem.tap.backupService
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayReducer
|
||||
import org.rekotlin.Action
|
||||
|
||||
class OnboardingWalletReducer {
|
||||
companion object {
|
||||
fun reduce(action: Action, state: AppState): OnboardingWalletState =
|
||||
internalReduce(action, state)
|
||||
fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -16,13 +17,17 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
|
|||
val state = appState.onboardingWalletState
|
||||
|
||||
return when (action) {
|
||||
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState))
|
||||
is GlobalAction.Onboarding.Start -> OnboardingWalletState(
|
||||
backupState = state.backupState.copy(canSkipBackup = action.fromHomeScreen)
|
||||
is GlobalAction.Onboarding -> ReducerForGlobalAction.reduce(action, state)
|
||||
is OnboardingSaltPayAction -> OnboardingSaltPayReducer.reduce(action, state)
|
||||
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState, state.isSaltPay))
|
||||
|
||||
is OnboardingWalletAction.GetToCreateWalletStep -> OnboardingWalletState(
|
||||
step = OnboardingWalletStep.CreateWallet,
|
||||
)
|
||||
is OnboardingWalletAction.GetToCreateWalletStep ->
|
||||
OnboardingWalletState(step = OnboardingWalletStep.CreateWallet)
|
||||
is OnboardingWalletAction.ProceedBackup -> state.copy(step = OnboardingWalletStep.Backup)
|
||||
is OnboardingWalletAction.GetToSaltPayStep -> state.copy(
|
||||
step = OnboardingWalletStep.SaltPay,
|
||||
)
|
||||
is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup)
|
||||
is OnboardingWalletAction.SetArtworkUrl -> {
|
||||
state.copy(cardArtworkUrl = action.artworkUrl)
|
||||
}
|
||||
|
|
@ -31,17 +36,47 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
|
|||
}
|
||||
}
|
||||
|
||||
private class ReducerForGlobalAction {
|
||||
companion object {
|
||||
fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState {
|
||||
return when (action) {
|
||||
is GlobalAction.Onboarding.Start -> {
|
||||
OnboardingWalletState(
|
||||
backupState = state.backupState.copy(
|
||||
maxBackupCards = if (action.scanResponse.isSaltPay()) 1 else 2,
|
||||
canSkipBackup = if (action.scanResponse.isSaltPay()) false else action.canSkipBackup,
|
||||
),
|
||||
isSaltPay = action.scanResponse.isSaltPay(),
|
||||
)
|
||||
}
|
||||
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
|
||||
OnboardingWalletState(
|
||||
backupState = state.backupState.copy(
|
||||
maxBackupCards = 2,
|
||||
canSkipBackup = false,
|
||||
),
|
||||
isSaltPay = false,
|
||||
)
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BackupReducer {
|
||||
private class BackupReducer {
|
||||
companion object {
|
||||
fun reduce(
|
||||
action: BackupAction, state: BackupState,
|
||||
action: BackupAction,
|
||||
state: BackupState,
|
||||
isSaltPay: Boolean,
|
||||
): BackupState {
|
||||
|
||||
return when (action) {
|
||||
is BackupAction.IntroduceBackup -> BackupState(
|
||||
backupStep = BackupStep.InitBackup,
|
||||
canSkipBackup = state.canSkipBackup,
|
||||
maxBackupCards = if (isSaltPay) 1 else 2,
|
||||
canSkipBackup = !isSaltPay,
|
||||
)
|
||||
BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard)
|
||||
BackupAction.StartAddingBackupCards -> {
|
||||
|
|
@ -50,9 +85,6 @@ class BackupReducer {
|
|||
BackupAction.AddBackupCard.Success -> {
|
||||
state.copy(backupCardsNumber = state.backupCardsNumber + 1)
|
||||
}
|
||||
BackupAction.FinishAddingBackupCards -> {
|
||||
state
|
||||
}
|
||||
BackupAction.ShowAccessCodeInfoScreen -> {
|
||||
state.copy(backupStep = BackupStep.SetAccessCode)
|
||||
}
|
||||
|
|
@ -62,13 +94,11 @@ class BackupReducer {
|
|||
is BackupAction.SaveFirstAccessCode -> {
|
||||
state.copy(
|
||||
backupStep = BackupStep.ReenterAccessCode,
|
||||
accessCode = action.accessCode
|
||||
accessCode = action.accessCode,
|
||||
)
|
||||
}
|
||||
is BackupAction.SetAccessCodeError -> {
|
||||
state.copy(
|
||||
accessCodeError = action.error
|
||||
)
|
||||
state.copy(accessCodeError = action.error)
|
||||
}
|
||||
is BackupAction.PrepareToWritePrimaryCard -> {
|
||||
if (state.primaryCardId == null) {
|
||||
|
|
@ -76,12 +106,10 @@ class BackupReducer {
|
|||
primaryCardId = backupService.primaryCardId,
|
||||
backupCardIds = backupService.backupCardIds,
|
||||
backupCardsNumber = backupService.backupCardIds.size,
|
||||
backupStep = BackupStep.WritePrimaryCard
|
||||
backupStep = BackupStep.WritePrimaryCard,
|
||||
)
|
||||
} else {
|
||||
state.copy(
|
||||
backupStep = BackupStep.WritePrimaryCard
|
||||
)
|
||||
state.copy(backupStep = BackupStep.WritePrimaryCard)
|
||||
}
|
||||
}
|
||||
is BackupAction.PrepareToWriteBackupCard -> {
|
||||
|
|
@ -90,43 +118,21 @@ class BackupReducer {
|
|||
primaryCardId = backupService.primaryCardId,
|
||||
backupCardIds = backupService.backupCardIds,
|
||||
backupCardsNumber = backupService.backupCardIds.size,
|
||||
backupStep = BackupStep.WriteBackupCard(action.cardNumber)
|
||||
backupStep = BackupStep.WriteBackupCard(action.cardNumber),
|
||||
)
|
||||
} else {
|
||||
state.copy(
|
||||
backupStep = BackupStep.WriteBackupCard(action.cardNumber)
|
||||
)
|
||||
state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber))
|
||||
}
|
||||
}
|
||||
|
||||
is BackupAction.FinishBackup -> {
|
||||
state.copy(
|
||||
backupStep = BackupStep.Finished
|
||||
)
|
||||
state.copy(backupStep = BackupStep.Finished)
|
||||
}
|
||||
|
||||
BackupAction.OnAccessCodeDialogClosed -> {
|
||||
state.copy(backupStep = BackupStep.AddBackupCards)
|
||||
}
|
||||
BackupAction.DiscardBackup -> BackupState()
|
||||
|
||||
BackupAction.WritePrimaryCard -> state
|
||||
is BackupAction.WriteBackupCard -> state
|
||||
is BackupAction.SaveAccessCodeConfirmation -> state
|
||||
BackupAction.ScanPrimaryCard -> state
|
||||
BackupAction.ShowEnterAccessCodeScreen -> state
|
||||
BackupAction.ShowReenterAccessCodeScreen -> state
|
||||
BackupAction.AddBackupCard -> state
|
||||
BackupAction.DismissBackup -> state
|
||||
is BackupAction.LoadBackupCardArtwork -> state
|
||||
is BackupAction.CheckAccessCode -> state
|
||||
BackupAction.DetermineBackupStep -> state
|
||||
BackupAction.CheckForUnfinishedBackup -> state
|
||||
BackupAction.ResumeBackup -> state
|
||||
BackupAction.DiscardSavedBackup -> state
|
||||
BackupAction.StartBackup -> state
|
||||
else -> state
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.redux
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.tangem.common.CardFilter
|
||||
import com.tangem.domain.common.SaltPayWorkaround
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayActivationStep
|
||||
import org.rekotlin.StateType
|
||||
|
||||
/**
|
||||
|
|
@ -10,42 +14,69 @@ import org.rekotlin.StateType
|
|||
data class OnboardingWalletState(
|
||||
val step: OnboardingWalletStep = OnboardingWalletStep.None,
|
||||
val backupState: BackupState = BackupState(),
|
||||
val onboardingSaltPayState: OnboardingSaltPayState? = null,
|
||||
val isSaltPay: Boolean = false,
|
||||
val cardArtworkUrl: String? = null,
|
||||
val showConfetti: Boolean = false,
|
||||
) : StateType {
|
||||
|
||||
val backupCardIdFilter: CardFilter.Companion.CardIdFilter?
|
||||
get() = when {
|
||||
isSaltPay -> CardFilter.Companion.CardIdFilter.Allow(
|
||||
items = SaltPayWorkaround.walletCardIds.toSet(),
|
||||
ranges = SaltPayWorkaround.walletCardIdRanges,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun getMaxProgress(): Int = when {
|
||||
isSaltPay -> 12
|
||||
else -> 6
|
||||
}
|
||||
|
||||
fun getProgressStep(): Int {
|
||||
return when {
|
||||
step == OnboardingWalletStep.CreateWallet -> {
|
||||
1
|
||||
}
|
||||
step == OnboardingWalletStep.CreateWallet -> 1
|
||||
step == OnboardingWalletStep.Backup -> {
|
||||
when (backupState.backupStep) {
|
||||
BackupStep.InitBackup -> 2
|
||||
BackupStep.ScanOriginCard -> 3
|
||||
|
||||
BackupStep.AddBackupCards -> 4
|
||||
BackupStep.EnterAccessCode -> 4
|
||||
BackupStep.ReenterAccessCode -> 4
|
||||
BackupStep.SetAccessCode -> 4
|
||||
BackupStep.WritePrimaryCard, is BackupStep.WriteBackupCard -> 5
|
||||
BackupStep.Finished -> 6
|
||||
BackupStep.Finished -> getMaxProgress()
|
||||
}
|
||||
}
|
||||
step == OnboardingWalletStep.Done -> 6
|
||||
step == OnboardingWalletStep.SaltPay && isSaltPay -> {
|
||||
when (onboardingSaltPayState!!.step) {
|
||||
SaltPayActivationStep.None, SaltPayActivationStep.NoGas -> 0
|
||||
SaltPayActivationStep.NeedPin -> 7
|
||||
SaltPayActivationStep.CardRegistration -> 8
|
||||
SaltPayActivationStep.KycIntro, SaltPayActivationStep.KycStart,
|
||||
SaltPayActivationStep.KycWaiting, SaltPayActivationStep.KycReject,
|
||||
-> 9
|
||||
SaltPayActivationStep.Claim -> 10
|
||||
SaltPayActivationStep.ClaimInProgress -> 11
|
||||
SaltPayActivationStep.Finished -> getMaxProgress()
|
||||
}
|
||||
}
|
||||
step == OnboardingWalletStep.Done -> getMaxProgress()
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class OnboardingWalletStep {
|
||||
None, CreateWallet, Backup, Done
|
||||
None, CreateWallet, Backup, SaltPay, Done
|
||||
}
|
||||
|
||||
data class BackupState(
|
||||
val primaryCardId: String? = null,
|
||||
val backupCardsNumber: Int = 0,
|
||||
val backupCardIds: List<CardId> = emptyList(),
|
||||
@Transient
|
||||
val backupCardsArtworks: Map<CardId, Bitmap> = emptyMap(),
|
||||
val accessCode: String = "",
|
||||
val accessCodeError: AccessCodeError? = null,
|
||||
|
|
@ -72,7 +103,7 @@ sealed class BackupStep {
|
|||
object Finished : BackupStep()
|
||||
}
|
||||
|
||||
sealed class BackupDialog: StateDialog {
|
||||
sealed class BackupDialog : StateDialog {
|
||||
object AddMoreBackupCards : StateDialog
|
||||
object BackupInProgress : StateDialog
|
||||
object UnfinishedBackupFound : StateDialog
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import com.tangem.blockchain.blockchains.ethereum.CompiledEthereumTransaction
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.extensions.successOr
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.domain.common.extensions.successOr
|
||||
import com.tangem.tap.common.extensions.isPositive
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.common.extensions.stub
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class GnosisRegistrator(
|
||||
val walletManager: EthereumWalletManager,
|
||||
) {
|
||||
|
||||
private val walletAddress = walletManager.wallet.address
|
||||
private val token = walletManager.wallet.getFirstToken().guard {
|
||||
throw NullPointerException("WalletManager for GnosisRegistrator must contain token")
|
||||
}
|
||||
|
||||
private val otpProcessorContractAddress: String = when (walletManager.wallet.blockchain) {
|
||||
Blockchain.SaltPay -> "0x3B4397C817A26521Df8bD01a949AFDE2251d91C2"
|
||||
Blockchain.SaltPayTestnet -> "0x710BF23486b549836509a08c184cE0188830f197"
|
||||
else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain")
|
||||
}
|
||||
|
||||
private val addressTreasureSafe = "0x8e9260a049d3Aa9ac60D0d4F27017320E0e2396B"
|
||||
|
||||
private val atomicNonce = AtomicLong()
|
||||
|
||||
suspend fun checkHasGas(): Result<Boolean> {
|
||||
val wallet = walletManager.safeUpdate().successOr {
|
||||
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
|
||||
}
|
||||
|
||||
val hasGas = wallet.amounts[AmountType.Coin]?.value?.isPositive() ?: false
|
||||
return Result.Success(hasGas)
|
||||
}
|
||||
|
||||
suspend fun prepareBeforeMakingTxs(): Result<Unit> {
|
||||
walletManager.safeUpdate().successOr {
|
||||
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
|
||||
}
|
||||
atomicNonce.set(walletManager.txCount)
|
||||
|
||||
return Result.Success(Unit)
|
||||
}
|
||||
|
||||
suspend fun sendTransactions(transactions: List<SignedEthereumTransaction>): Result<List<String>> {
|
||||
val results = transactions.map {
|
||||
coroutineScope { async { walletManager.sendRaw(it.compiledTransaction, it.signature) } }
|
||||
}.awaitAll()
|
||||
|
||||
val errors = results.filterIsInstance<SimpleResult.Failure>()
|
||||
return if (errors.isEmpty()) {
|
||||
Result.Success(listOf())
|
||||
} else {
|
||||
Result.Failure(errors[0].error)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun makeSetSpendLimitTx(value: BigDecimal): Result<CompiledEthereumTransaction> {
|
||||
val limitAmount = Amount(walletManager.cardTokens.first(), value)
|
||||
val feeAmount = walletManager.getFeeToSetSpendLimit(otpProcessorContractAddress, limitAmount)
|
||||
.extractFeeAmount().successOr { return it }
|
||||
|
||||
val compiledEthereumTransaction = walletManager.transactionBuilder.buildSetSpendLimitToSign(
|
||||
processorContractAddress = otpProcessorContractAddress,
|
||||
cardAddress = walletAddress,
|
||||
amount = limitAmount,
|
||||
transactionFee = feeAmount,
|
||||
gasLimit = walletManager.gasLimitToSetSpendLimit,
|
||||
nonce = atomicNonce.getAndIncrement().toBigInteger(),
|
||||
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'setSpendLimit' transaction"))
|
||||
|
||||
return Result.Success(compiledEthereumTransaction)
|
||||
}
|
||||
|
||||
suspend fun makeInitOtpTx(rootOTP: ByteArray, rootOTPCounter: Int): Result<CompiledEthereumTransaction> {
|
||||
val feeAmount = walletManager.getFeeToInitOTP(otpProcessorContractAddress, rootOTP, rootOTPCounter)
|
||||
.extractFeeAmount().successOr { return it }
|
||||
|
||||
val compiledEthereumTransaction = walletManager.transactionBuilder.buildInitOTPToSign(
|
||||
processorContractAddress = otpProcessorContractAddress,
|
||||
cardAddress = walletAddress,
|
||||
otp = rootOTP,
|
||||
otpCounter = rootOTPCounter,
|
||||
transactionFee = feeAmount,
|
||||
gasLimit = walletManager.gasLimitToInitOTP,
|
||||
nonce = atomicNonce.getAndIncrement().toBigInteger(),
|
||||
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'initOtp' transaction"))
|
||||
|
||||
return Result.Success(compiledEthereumTransaction)
|
||||
}
|
||||
|
||||
suspend fun makeSetWalletTx(): Result<CompiledEthereumTransaction> {
|
||||
val feeAmount = walletManager.getFeeToSetWallet(otpProcessorContractAddress)
|
||||
.extractFeeAmount().successOr { return it }
|
||||
|
||||
val compiledEthereumTransaction = walletManager.transactionBuilder.buildSetWalletToSign(
|
||||
processorContractAddress = otpProcessorContractAddress,
|
||||
cardAddress = walletAddress,
|
||||
transactionFee = feeAmount,
|
||||
gasLimit = walletManager.gasLimitToSetWallet,
|
||||
nonce = atomicNonce.getAndIncrement().toBigInteger(),
|
||||
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'setWallet' transaction"))
|
||||
|
||||
return Result.Success(compiledEthereumTransaction)
|
||||
}
|
||||
|
||||
suspend fun makeApprovalTx(value: BigDecimal): Result<CompiledEthereumTransaction> {
|
||||
val approveAmount = Amount(token, value)
|
||||
val feeAmount = walletManager.getFeeToApprove(approveAmount, otpProcessorContractAddress)
|
||||
.extractFeeAmount().successOr { return it }
|
||||
|
||||
val compiledEthereumTransaction = walletManager.transactionBuilder.buildApproveToSign(
|
||||
transactionData = walletManager.createTransaction(
|
||||
amount = approveAmount,
|
||||
fee = feeAmount,
|
||||
destination = otpProcessorContractAddress,
|
||||
),
|
||||
nonce = atomicNonce.getAndIncrement().toBigInteger(),
|
||||
gasLimit = walletManager.gasLimitToApprove,
|
||||
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'approval' transaction"))
|
||||
|
||||
return Result.Success(compiledEthereumTransaction)
|
||||
}
|
||||
|
||||
suspend fun getAllowance(): Result<Amount> {
|
||||
return walletManager.getAllowance(addressTreasureSafe, token)
|
||||
}
|
||||
|
||||
suspend fun transferFrom(amountToClaim: BigDecimal, signer: TransactionSigner): Result<Unit> {
|
||||
val amount = Amount(token, amountToClaim)
|
||||
val feeAmount = walletManager.getFeeToTransferFrom(amount, addressTreasureSafe)
|
||||
.extractFeeAmount().successOr { return it }
|
||||
|
||||
val transactionData = walletManager.createTransferFromTransaction(amount, feeAmount, addressTreasureSafe)
|
||||
walletManager.transferFrom(transactionData, signer).successOr { return Result.Failure(it.error) }
|
||||
|
||||
return Result.Success(Unit)
|
||||
}
|
||||
|
||||
private fun Result<List<Amount>>.extractFeeAmount(): Result<Amount> {
|
||||
return when (this) {
|
||||
is Result.Success -> {
|
||||
if (this.data.size != 3) Result.Failure(BlockchainSdkError.FailedToLoadFee)
|
||||
else Result.Success(this.data[1])
|
||||
}
|
||||
is Result.Failure -> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long?.toBigInteger(): BigInteger? = this?.let { BigInteger.valueOf(it) }
|
||||
|
||||
private fun Long.toBigInteger(): BigInteger = BigInteger.valueOf(this)
|
||||
|
||||
companion object {
|
||||
fun stub(): GnosisRegistrator {
|
||||
return GnosisRegistrator(WalletManager.stub())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import com.tangem.common.Filter
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class PinFilter : Filter<String>
|
||||
|
||||
class AllSymbolsTheSameFilter : PinFilter() {
|
||||
override fun filter(value: String): Boolean {
|
||||
if (value.isEmpty()) return false
|
||||
|
||||
val array = value.toCharArray()
|
||||
val firstSymbol = array[0]
|
||||
return array.all { it == firstSymbol }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import android.net.Uri
|
||||
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.extensions.successOr
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.extensions.successOr
|
||||
import com.tangem.network.api.paymentology.AttestationResponse
|
||||
import com.tangem.network.api.paymentology.PaymentologyApiService
|
||||
import com.tangem.network.api.paymentology.RegisterKYCRequest
|
||||
import com.tangem.network.api.paymentology.RegisterWalletRequest
|
||||
import com.tangem.network.api.paymentology.RegisterWalletResponse
|
||||
import com.tangem.network.api.paymentology.RegistrationResponse
|
||||
import com.tangem.operations.attestation.AttestWalletKeyResponse
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import com.tangem.tap.domain.tokens.UserWalletId
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
import com.tangem.tap.persistence.SaltPayActivationStorage
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class SaltPayActivationManager(
|
||||
val cardId: String,
|
||||
val cardPublicKey: ByteArray,
|
||||
val walletPublicKey: ByteArray,
|
||||
private val kycProvider: KYCProvider,
|
||||
private val paymentologyService: PaymentologyApiService,
|
||||
private val gnosisRegistrator: GnosisRegistrator,
|
||||
private val registrationStorage: SaltPayActivationStorage,
|
||||
) {
|
||||
val kycUrlProvider = KYCUrlProvider(walletPublicKey, kycProvider)
|
||||
|
||||
private val approvalValue: BigDecimal = BigDecimal(2).pow(256).minus(BigDecimal.ONE)
|
||||
.movePointLeft(gnosisRegistrator.walletManager.wallet.blockchain.decimals())
|
||||
|
||||
private val spendLimitValue: BigDecimal = BigDecimal("100")
|
||||
|
||||
fun transactionIsSent(): Boolean {
|
||||
return registrationStorage.data.transactionsSent
|
||||
}
|
||||
|
||||
suspend fun checkHasGas(): Result<Unit> {
|
||||
return when (val hasGasResult = gnosisRegistrator.checkHasGas()) {
|
||||
is com.tangem.blockchain.extensions.Result.Success -> if (hasGasResult.data) {
|
||||
Result.Success(Unit)
|
||||
} else {
|
||||
Result.Failure(SaltPayActivationError.NoGas)
|
||||
}
|
||||
is com.tangem.blockchain.extensions.Result.Failure -> Result.Failure(SaltPayActivationError.NoGas)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun registerKYC(): Result<Unit> {
|
||||
return when (val result = paymentologyService.registerKYC(makeRegisterKYCRequest())) {
|
||||
is Result.Success -> Result.Success(Unit)
|
||||
is Result.Failure -> result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkRegistration(): Result<RegistrationResponse.Item> {
|
||||
val registrationResponse = paymentologyService.checkRegistration(cardId, cardPublicKey).successOr { return it }
|
||||
|
||||
return try {
|
||||
if (!registrationResponse.success || registrationResponse.results.isEmpty()) {
|
||||
throw SaltPayActivationError.EmptyResponse(registrationResponse.error)
|
||||
}
|
||||
Result.Success(registrationResponse.results[0])
|
||||
} catch (ex: SaltPayActivationError) {
|
||||
Result.Failure(ex)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requestAttestationChallenge(): Result<AttestationResponse> {
|
||||
return paymentologyService.requestAttestationChallenge(cardId, cardPublicKey)
|
||||
}
|
||||
|
||||
suspend fun sendTransactions(
|
||||
signedTransactions: List<SignedEthereumTransaction>,
|
||||
): com.tangem.blockchain.extensions.Result<List<String>> {
|
||||
return gnosisRegistrator.sendTransactions(signedTransactions)
|
||||
}
|
||||
|
||||
suspend fun registerWallet(
|
||||
attestResponse: AttestWalletKeyResponse,
|
||||
pinCode: String,
|
||||
): Result<RegisterWalletResponse> {
|
||||
val request = RegisterWalletRequest(
|
||||
cardId = cardId,
|
||||
publicKey = cardPublicKey,
|
||||
walletPublicKey = walletPublicKey,
|
||||
walletSalt = attestResponse.salt,
|
||||
walletSignature = attestResponse.walletSignature,
|
||||
cardSalt = attestResponse.publicKeySalt ?: byteArrayOf(),
|
||||
cardSignature = attestResponse.cardSignature ?: byteArrayOf(),
|
||||
pin = pinCode,
|
||||
)
|
||||
|
||||
val result = paymentologyService.registerWallet(request)
|
||||
|
||||
registrationStorage.data = registrationStorage.data.copy(
|
||||
transactionsSent = result is Result.Success,
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun getAmountToClaim(): Result<Amount> {
|
||||
val amount = gnosisRegistrator.getAllowance().successOr {
|
||||
return Result.Failure(SaltPayActivationError.FailedToGetFundsToClaim)
|
||||
}
|
||||
|
||||
return if (amount.value == null || amount.value?.isZero() == true) {
|
||||
Result.Failure(SaltPayActivationError.NoFundsToClaim)
|
||||
} else {
|
||||
Result.Success(amount)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun claim(amountToClaim: BigDecimal, signer: TransactionSigner): Result<Unit> {
|
||||
val result = gnosisRegistrator.transferFrom(amountToClaim, signer).successOr {
|
||||
return Result.Failure(SaltPayActivationError.ClaimTransactionFailed)
|
||||
}
|
||||
return Result.Success(Unit)
|
||||
}
|
||||
|
||||
suspend fun getTokenAmount(): Result<BigDecimal> {
|
||||
val wallet = gnosisRegistrator.walletManager.safeUpdate().successOr { return it }
|
||||
val token = wallet.getFirstToken().guard {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
val amountValue = wallet.amounts[AmountType.Token(token)]?.value.guard {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
return Result.Success(amountValue)
|
||||
}
|
||||
|
||||
fun makeRegistrationTask(challenge: ByteArray): SaltPayRegistrationTask {
|
||||
return SaltPayRegistrationTask(
|
||||
gnosisRegistrator = gnosisRegistrator,
|
||||
challenge = challenge,
|
||||
walletPublicKey = walletPublicKey,
|
||||
approvalValue = approvalValue,
|
||||
spendLimitValue = spendLimitValue,
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeRegisterKYCRequest(): RegisterKYCRequest = RegisterKYCRequest(
|
||||
cardId = cardId,
|
||||
publicKey = cardPublicKey,
|
||||
kycProvider = "UTORG",
|
||||
kycRefId = kycUrlProvider.kycRefId,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun stub(): SaltPayActivationManager = SaltPayActivationManager(
|
||||
kycProvider = SaltPayConfig.stub().kycProvider,
|
||||
paymentologyService = PaymentologyApiService.stub(),
|
||||
gnosisRegistrator = GnosisRegistrator.stub(),
|
||||
registrationStorage = SaltPayActivationStorage.stub(),
|
||||
cardId = "",
|
||||
cardPublicKey = byteArrayOf(),
|
||||
walletPublicKey = byteArrayOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class KYCUrlProvider(
|
||||
walletPublicKey: ByteArray,
|
||||
kycProvider: KYCProvider,
|
||||
) {
|
||||
|
||||
val kycRefId = UserWalletId(walletPublicKey).stringValue
|
||||
|
||||
val requestUrl: String = makeWebRequestUrl(kycProvider)
|
||||
|
||||
val doneUrl = "https://success.tangem.com/"
|
||||
|
||||
private fun makeWebRequestUrl(kycProvider: KYCProvider): String {
|
||||
val baseUri = Uri.parse(kycProvider.baseUrl)
|
||||
return Uri.Builder()
|
||||
.scheme(baseUri.scheme)
|
||||
.authority(baseUri.authority)
|
||||
.encodedPath(baseUri.path)
|
||||
.appendQueryParameter(kycProvider.sidParameterKey, kycProvider.sidValue)
|
||||
.appendQueryParameter(kycProvider.externalIdParameterKey, kycRefId)
|
||||
.build().toString()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class SaltPayConfig(
|
||||
val zendesk: ZendeskConfig,
|
||||
val kycProvider: KYCProvider,
|
||||
) {
|
||||
companion object {
|
||||
fun stub(): SaltPayConfig {
|
||||
return SaltPayConfig(
|
||||
zendesk = ZendeskConfig("", "", "", "", ""),
|
||||
kycProvider = KYCProvider("", "", "", ""),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class KYCProvider(
|
||||
val baseUrl: String,
|
||||
val externalIdParameterKey: String,
|
||||
val sidParameterKey: String,
|
||||
val sidValue: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
import com.tangem.tap.store
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class SaltPayExceptionHandler {
|
||||
companion object {
|
||||
fun handle(throwable: Throwable) {
|
||||
when (throwable) {
|
||||
is SaltPayActivationError -> {
|
||||
val dialog = when (throwable) {
|
||||
is SaltPayActivationError.NoGas -> SaltPayDialog.Activation.NoGas
|
||||
else -> SaltPayDialog.Activation.OnError(throwable)
|
||||
}
|
||||
store.dispatchDialogShow(dialog)
|
||||
}
|
||||
|
||||
is TangemSdkError -> {
|
||||
when (throwable) {
|
||||
is TangemSdkError.NetworkError -> {
|
||||
val message = (throwable).customMessage
|
||||
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message))
|
||||
}
|
||||
else -> {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val message = throwable.message ?: "SaltPay unknown error"
|
||||
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import com.tangem.blockchain.blockchains.ethereum.CompiledEthereumTransaction
|
||||
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
|
||||
import com.tangem.blockchain.extensions.successOr
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.CompletionCallback
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.operations.CommandResponse
|
||||
import com.tangem.operations.GenerateOTPCommand
|
||||
import com.tangem.operations.GenerateOTPResponse
|
||||
import com.tangem.operations.attestation.AttestWalletKeyCommand
|
||||
import com.tangem.operations.attestation.AttestWalletKeyResponse
|
||||
import com.tangem.operations.sign.SignHashesCommand
|
||||
import com.tangem.tap.scope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class RegistrationTaskResponse(
|
||||
val signedTransactions: List<SignedEthereumTransaction>,
|
||||
val attestResponse: AttestWalletKeyResponse,
|
||||
) : CommandResponse
|
||||
|
||||
class SaltPayRegistrationTask(
|
||||
private var gnosisRegistrator: GnosisRegistrator,
|
||||
private var challenge: ByteArray,
|
||||
private val walletPublicKey: ByteArray,
|
||||
private val approvalValue: BigDecimal,
|
||||
private val spendLimitValue: BigDecimal,
|
||||
) : CardSessionRunnable<RegistrationTaskResponse> {
|
||||
|
||||
private var generateOTPResponse: GenerateOTPResponse? = null
|
||||
private var attestWalletResponse: AttestWalletKeyResponse? = null
|
||||
private var signedTransactions: List<SignedEthereumTransaction> = listOf()
|
||||
|
||||
override fun run(session: CardSession, callback: CompletionCallback<RegistrationTaskResponse>) {
|
||||
generateOTP(session, callback)
|
||||
}
|
||||
|
||||
private fun generateOTP(session: CardSession, callback: CompletionCallback<RegistrationTaskResponse>) {
|
||||
GenerateOTPCommand().run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
generateOTPResponse = result.data
|
||||
attestWallet(session, callback)
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attestWallet(
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<RegistrationTaskResponse>,
|
||||
) {
|
||||
val card = session.environment.card.guard {
|
||||
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
|
||||
return
|
||||
}
|
||||
|
||||
val walletPublicKey = card.wallets.firstOrNull()?.publicKey
|
||||
if (walletPublicKey == null || !walletPublicKey.contentEquals(this.walletPublicKey)) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.WalletNotFound()))
|
||||
return
|
||||
}
|
||||
|
||||
val attestWalletCommand = AttestWalletKeyCommand(
|
||||
publicKey = walletPublicKey,
|
||||
challenge = challenge,
|
||||
confirmationMode = AttestWalletKeyCommand.ConfirmationMode.Dynamic,
|
||||
)
|
||||
attestWalletCommand.run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
attestWalletResponse = result.data
|
||||
scope.launch { prepareTransactions(session, callback) }
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prepareTransactions(
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<RegistrationTaskResponse>,
|
||||
) {
|
||||
val generateOTPResponse = this.generateOTPResponse.guard {
|
||||
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
|
||||
return
|
||||
}
|
||||
|
||||
gnosisRegistrator.prepareBeforeMakingTxs().successOr {
|
||||
callback(CompletionResult.Failure(it.error))
|
||||
return
|
||||
}
|
||||
|
||||
val approvalTx = gnosisRegistrator.makeApprovalTx(approvalValue).successOr {
|
||||
callback(CompletionResult.Failure(it.error))
|
||||
return
|
||||
}
|
||||
val setWalletTx = gnosisRegistrator.makeSetWalletTx().successOr {
|
||||
callback(CompletionResult.Failure(it.error))
|
||||
return
|
||||
}
|
||||
val initOtpTx = gnosisRegistrator.makeInitOtpTx(
|
||||
generateOTPResponse.rootOTP,
|
||||
generateOTPResponse.rootOTPCounter,
|
||||
).successOr {
|
||||
callback(CompletionResult.Failure(it.error))
|
||||
return
|
||||
}
|
||||
|
||||
val setSpendLimitTx = gnosisRegistrator.makeSetSpendLimitTx(spendLimitValue).successOr {
|
||||
callback(CompletionResult.Failure(it.error))
|
||||
return
|
||||
}
|
||||
|
||||
signTransactions(
|
||||
transactions = listOf(
|
||||
approvalTx,
|
||||
setWalletTx,
|
||||
initOtpTx,
|
||||
setSpendLimitTx,
|
||||
),
|
||||
session = session,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
|
||||
private fun signTransactions(
|
||||
transactions: List<CompiledEthereumTransaction>,
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<RegistrationTaskResponse>,
|
||||
) {
|
||||
val walletPublicKey = session.environment.card?.wallets?.firstOrNull()?.publicKey.guard {
|
||||
callback(CompletionResult.Failure(TangemSdkError.WalletNotFound()))
|
||||
return
|
||||
}
|
||||
|
||||
val hashes = transactions.map { it.hash }.toTypedArray()
|
||||
val signHashesCommand = SignHashesCommand(hashes, walletPublicKey)
|
||||
|
||||
signHashesCommand.run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
signedTransactions = transactions.zip(result.data.signatures).map {
|
||||
SignedEthereumTransaction(it.first, it.second)
|
||||
}
|
||||
complete(callback)
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun complete(callback: CompletionCallback<RegistrationTaskResponse>) {
|
||||
if (signedTransactions.isEmpty() || attestWalletResponse == null) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
|
||||
} else {
|
||||
val result = RegistrationTaskResponse(signedTransactions, attestWalletResponse!!)
|
||||
callback(CompletionResult.Success(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.text.TextUtils
|
||||
import android.webkit.PermissionRequest
|
||||
import android.webkit.ValueCallback
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import com.tangem.tap.common.ActivityResultCallbackHolder
|
||||
import com.tangem.tap.common.OnActivityResultCallback
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.isPermissionGranted
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class UtorgWebViewClient(
|
||||
private val onSuccess: () -> Unit,
|
||||
private val successUrl: String,
|
||||
) : WebViewClient() {
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean =
|
||||
if (successUrl == request.url.toString()) {
|
||||
onSuccess()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
class UtorgWebChromeClient(
|
||||
private val activity: ComponentActivity,
|
||||
private val activityResultCallbackHolder: ActivityResultCallbackHolder = activity as ActivityResultCallbackHolder,
|
||||
) : WebChromeClient() {
|
||||
|
||||
private val fileChooseRequestCode = 1002
|
||||
private val permissionRequestCode = 1003
|
||||
private val onActivityResultCallback: OnActivityResultCallback = ::onActivityResult
|
||||
|
||||
private val permissionList = listOf(
|
||||
"android.permission.CAMERA",
|
||||
)
|
||||
|
||||
private var filePathCallback: ValueCallback<Array<Uri>>? = null
|
||||
private var mediaUri: Uri? = null
|
||||
|
||||
override fun onShowFileChooser(
|
||||
webView: WebView?,
|
||||
filePathCallback: ValueCallback<Array<Uri>>,
|
||||
fileChooserParams: FileChooserParams?,
|
||||
): Boolean {
|
||||
activityResultCallbackHolder.addOnActivityResultCallback(onActivityResultCallback)
|
||||
if (!readyToShowFileChooser()) return false
|
||||
|
||||
val type: String
|
||||
if (fileChooserParams != null && fileChooserParams.acceptTypes != null && fileChooserParams.acceptTypes.isNotEmpty()) {
|
||||
type = if (!TextUtils.isEmpty(fileChooserParams.acceptTypes[0])) fileChooserParams.acceptTypes[0] else "*/*"
|
||||
this.filePathCallback = filePathCallback
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
proceedOnType(type, fileChooserParams.isCaptureEnabled)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun readyToShowFileChooser(): Boolean {
|
||||
val permissionsToGrant = permissionList.filterNot { activity.isPermissionGranted(it) }.toTypedArray()
|
||||
return if (permissionsToGrant.isNotEmpty()) {
|
||||
ActivityCompat.requestPermissions(activity, permissionsToGrant, permissionRequestCode)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedOnType(type: String, isCaptureEnabled: Boolean) {
|
||||
if (isCaptureEnabled) {
|
||||
mediaUri = Uri.fromFile(File(getImageCaptureCachePath()))
|
||||
val imageCaptureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||
imageCaptureIntent.putExtra("output", mediaUri)
|
||||
activity.startActivityForResult(
|
||||
Intent.createChooser(imageCaptureIntent, "Image Chooser"),
|
||||
fileChooseRequestCode,
|
||||
)
|
||||
} else {
|
||||
mediaUri = Uri.fromFile(File(getImageCaptureCachePath()))
|
||||
|
||||
val getContentIntent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
getContentIntent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
getContentIntent.type = "image/*"
|
||||
|
||||
// val imageCaptureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||
// imageCaptureIntent.putExtra("output", mediaUri)
|
||||
|
||||
val chooserIntent = Intent.createChooser(getContentIntent, "Pick a photo")
|
||||
// chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(imageCaptureIntent))
|
||||
|
||||
activity.startActivityForResult(
|
||||
Intent.createChooser(chooserIntent, "Image Chooser"),
|
||||
fileChooseRequestCode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
fileChooseRequestCode -> {
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
filePathCallback?.onReceiveValue(null)
|
||||
clean()
|
||||
return
|
||||
}
|
||||
if (data == null) {
|
||||
filePathCallback?.onReceiveValue(arrayOf(mediaUri!!))
|
||||
} else {
|
||||
val uri = FileChooserParams.parseResult(resultCode, data)
|
||||
filePathCallback?.onReceiveValue(uri)
|
||||
}
|
||||
clean()
|
||||
}
|
||||
permissionRequestCode -> {
|
||||
val notGranted = permissionList.filterNot { activity.isPermissionGranted(it) }
|
||||
if (notGranted.isNotEmpty()) {
|
||||
val permissionsName = notGranted.joinToString("\n") { it.permissionName() }
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialog(
|
||||
header = activity.getString(R.string.common_warning),
|
||||
message = "Permission access is required to pass authorization." +
|
||||
"To grant permission \n" +
|
||||
"$permissionsName \n" +
|
||||
"you must enable them in the app's settings",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPermissionRequest(request: PermissionRequest) {
|
||||
request.grant(request.resources)
|
||||
}
|
||||
|
||||
private fun getImageCaptureCachePath(): String = "cached_photo_" + System.currentTimeMillis() + ".jpg"
|
||||
|
||||
private fun clean() {
|
||||
activityResultCallbackHolder.removeOnActivityResultCallback(onActivityResultCallback)
|
||||
filePathCallback = null
|
||||
mediaUri = null
|
||||
}
|
||||
|
||||
private fun String.permissionName(): String {
|
||||
return this.slice(this.lastIndexOf(".") until this.length)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class NoFundsForActivationDialog {
|
||||
companion object {
|
||||
fun create(context: Context): Dialog {
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.saltpay_error_no_gas_title)
|
||||
setMessage(R.string.saltpay_error_no_gas_message)
|
||||
//TODO: SaltPay: change onboarding_supplement_button_kyc_waiting -> to appropriate string
|
||||
setPositiveButton(R.string.onboarding_supplement_button_kyc_waiting) { _, _ ->
|
||||
val config = store.state.globalState.configManager?.config?.saltPayConfig?.zendesk.guard {
|
||||
store.dispatchDebugErrorNotification("SaltPayConfig not initialized")
|
||||
return@setPositiveButton
|
||||
}
|
||||
store.dispatch(GlobalAction.OpenChat(SupportInfo(), config))
|
||||
}
|
||||
setNegativeButton(R.string.common_cancel) { _, _ ->
|
||||
}
|
||||
setOnDismissListener {
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
}
|
||||
setCancelable(false)
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.moduleMessage.ConvertedDialogMessage
|
||||
import com.tangem.tap.domain.moduleMessage.saltPay.SaltPayErrorConverter
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class RegistrationErrorDialog {
|
||||
companion object {
|
||||
fun create(context: Context, dialog: SaltPayDialog.Activation.OnError): Dialog {
|
||||
val convertedMessage = SaltPayErrorConverter(context).convert(dialog.error) as ConvertedDialogMessage
|
||||
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setTitle(convertedMessage.title)
|
||||
setMessage(convertedMessage.message)
|
||||
setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
}
|
||||
setOnDismissListener {
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
}
|
||||
setCancelable(false)
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog
|
||||
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class SaltPayDialog : StateDialog {
|
||||
sealed class Activation : SaltPayDialog() {
|
||||
object NoGas : SaltPayDialog()
|
||||
data class OnError(val error: SaltPayActivationError) : SaltPayDialog()
|
||||
data class TryToInterrupt(val onOk: VoidCallback, val onCancel: VoidCallback) : SaltPayDialog()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class TryToInterruptRegistrationDialog {
|
||||
companion object {
|
||||
fun create(context: Context, dialog: SaltPayDialog.Activation.TryToInterrupt): Dialog {
|
||||
return AlertDialog.Builder(context).apply {
|
||||
setTitle(context.getString(R.string.onboarding_exit_alert_title))
|
||||
setMessage(context.getString(R.string.onboarding_exit_alert_message))
|
||||
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onOk() }
|
||||
setNegativeButton(R.string.common_cancel) { _, _ -> dialog.onCancel() }
|
||||
setOnDismissListener { store.dispatchDialogHide() }
|
||||
setCancelable(false)
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.message
|
||||
|
||||
import com.tangem.common.module.ModuleError
|
||||
import com.tangem.common.module.ModuleErrorCode
|
||||
import com.tangem.common.module.ModuleMessage
|
||||
|
||||
sealed interface SaltPayModuleMessage : ModuleMessage
|
||||
|
||||
sealed class SaltPayModuleError(
|
||||
subCode: Int,
|
||||
override val message: String,
|
||||
override val data: Any? = null,
|
||||
) : SaltPayModuleMessage, ModuleError() {
|
||||
override val code: Int = ModuleErrorCode.SALT_PAY + subCode
|
||||
|
||||
companion object {
|
||||
// base code used for all errors in the module
|
||||
internal const val ERROR_CODE_REGISTRATION = 100
|
||||
// const val CODE_ANY_OTHER = 200..299, 300..399 etc
|
||||
}
|
||||
}
|
||||
|
||||
sealed class SaltPayActivationError(
|
||||
subCode: Int,
|
||||
message: String? = null,
|
||||
override val data: Any? = null,
|
||||
) : SaltPayModuleError(
|
||||
subCode = ERROR_CODE_REGISTRATION + subCode,
|
||||
message = message ?: EMPTY_MESSAGE,
|
||||
) {
|
||||
object Unknown : SaltPayActivationError(4)
|
||||
object Empty : SaltPayActivationError(4)
|
||||
|
||||
// gnosis
|
||||
object FailedToMakeTxData : SaltPayActivationError(1)
|
||||
object FailedToSendTx : SaltPayActivationError(2)
|
||||
|
||||
object NeedPin : SaltPayActivationError(3)
|
||||
object NoGas : SaltPayActivationError(5) //
|
||||
class EmptyResponse(message: String?) : SaltPayActivationError(6, message)
|
||||
class CardNotFound(message: String?) : SaltPayActivationError(8, message)
|
||||
class CardDisabled(message: String?) : SaltPayActivationError(9, message)
|
||||
class CardNotPassed(message: String?) : SaltPayActivationError(10, message)
|
||||
object EmptyDynamicAttestResponse : SaltPayActivationError(11)
|
||||
object EmptyBackupCardScanned : SaltPayActivationError(12)
|
||||
object WeakPin : SaltPayActivationError(13)
|
||||
|
||||
object FailedToGetFundsToClaim : SaltPayActivationError(14)
|
||||
object NoFundsToClaim : SaltPayActivationError(15)
|
||||
object ClaimTransactionFailed : SaltPayActivationError(16)
|
||||
object NotClaimed : SaltPayActivationError(17)
|
||||
|
||||
companion object {
|
||||
const val EMPTY_MESSAGE = ""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class OnboardingSaltPayAction : Action {
|
||||
sealed class Init : OnboardingSaltPayAction() {
|
||||
data class SetDependencies(
|
||||
val registrationManager: SaltPayActivationManager,
|
||||
val saltPayConfig: SaltPayConfig,
|
||||
) : Init()
|
||||
}
|
||||
|
||||
data class SetInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction()
|
||||
data class SetClaimRefreshInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction()
|
||||
|
||||
data class SetAccessCode(val accessCode: String?) : OnboardingSaltPayAction()
|
||||
|
||||
object Update : OnboardingSaltPayAction()
|
||||
object RegisterCard : OnboardingSaltPayAction()
|
||||
object OpenUtorgKYC : OnboardingSaltPayAction()
|
||||
object UtorgKYCRedirectSuccess : OnboardingSaltPayAction()
|
||||
object RegisterKYC : OnboardingSaltPayAction()
|
||||
object Claim : OnboardingSaltPayAction()
|
||||
object RefreshClaim : OnboardingSaltPayAction()
|
||||
|
||||
data class TrySetPin(val pin: String) : OnboardingSaltPayAction()
|
||||
data class SetPin(val pin: String) : OnboardingSaltPayAction()
|
||||
|
||||
data class SetStep(val newStep: SaltPayActivationStep? = null) : OnboardingSaltPayAction()
|
||||
data class SetAmountToClaim(val amount: Amount?) : OnboardingSaltPayAction()
|
||||
data class SetTokenBalance(val balanceValue: BigDecimal) : OnboardingSaltPayAction()
|
||||
}
|
||||
|
|
@ -0,0 +1,416 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
|
||||
|
||||
import com.tangem.Message
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.extensions.successOr
|
||||
import com.tangem.common.Filter
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.extensions.successOr
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.network.api.paymentology.KYCStatus
|
||||
import com.tangem.network.api.paymentology.RegistrationResponse
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.TangemSigner
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.AllSymbolsTheSameFilter
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdk
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class OnboardingSaltPayMiddleware {
|
||||
companion object {
|
||||
val handler = onboardingSaltPayMiddleware
|
||||
}
|
||||
}
|
||||
|
||||
private val onboardingSaltPayMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
handleOnboardingSaltPayAction(action, state)
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> AppState?) {
|
||||
if (DemoHelper.tryHandle(appState, anyAction)) return
|
||||
val action = anyAction as? OnboardingSaltPayAction ?: return
|
||||
appState() ?: return
|
||||
|
||||
fun getAppState(): AppState = appState()!!
|
||||
fun getOnboardingWalletState(): OnboardingWalletState = getAppState().onboardingWalletState
|
||||
fun getState(): OnboardingSaltPayState = getOnboardingWalletState().onboardingSaltPayState!!
|
||||
|
||||
when (action) {
|
||||
is OnboardingSaltPayAction.Update -> {
|
||||
handleInProgress = true
|
||||
|
||||
val state = getState()
|
||||
scope.launch {
|
||||
val updateResult = state.saltPayManager.updateActivationStatus(
|
||||
amountToClaim = state.amountToClaim,
|
||||
step = state.step,
|
||||
).successOr {
|
||||
onException(it.error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
handleInProgress = false
|
||||
withMainContext {
|
||||
store.dispatch(OnboardingSaltPayAction.SetStep(updateResult.step))
|
||||
}
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.RegisterCard -> {
|
||||
handleInProgress = true
|
||||
val state = getState()
|
||||
|
||||
scope.launch {
|
||||
val saltPayManager = state.saltPayManager
|
||||
checkGasIfNeeded(state.saltPayManager, state.step).successOr {
|
||||
onException(it.error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val pinCode = state.pinCode.guard {
|
||||
onError(SaltPayActivationError.NeedPin)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val attestationResponse = saltPayManager.requestAttestationChallenge().successOr {
|
||||
onException(it.error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!attestationResponse.success) {
|
||||
onError(SaltPayActivationError.CardNotFound(attestationResponse.error))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val registrationResponse = tangemSdkManager.runTaskAsync(
|
||||
runnable = saltPayManager.makeRegistrationTask(attestationResponse.challenge!!),
|
||||
cardId = saltPayManager.cardId,
|
||||
initialMessage = Message(
|
||||
header = null,
|
||||
body = tangemSdkManager.getString(R.string.registration_task_alert_message),
|
||||
),
|
||||
accessCode = state.accessCode,
|
||||
).successOr {
|
||||
onException(it.error as Exception)
|
||||
return@launch
|
||||
}
|
||||
|
||||
saltPayManager.sendTransactions(registrationResponse.signedTransactions).successOr {
|
||||
onError(SaltPayActivationError.FailedToSendTx)
|
||||
return@launch
|
||||
}
|
||||
|
||||
saltPayManager.registerWallet(registrationResponse.attestResponse, pinCode).successOr {
|
||||
onException(it.error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
withMainContext {
|
||||
handleInProgress = false
|
||||
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycIntro))
|
||||
}
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.OpenUtorgKYC -> {
|
||||
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycStart))
|
||||
}
|
||||
is OnboardingSaltPayAction.UtorgKYCRedirectSuccess -> {
|
||||
store.dispatch(OnboardingSaltPayAction.RegisterKYC)
|
||||
}
|
||||
is OnboardingSaltPayAction.RegisterKYC -> {
|
||||
val state = getState()
|
||||
handleInProgress = true
|
||||
scope.launch {
|
||||
registerKYCIfNeeded(state.saltPayManager, state.step).successOr {
|
||||
onException(it.error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
handleInProgress = false
|
||||
withMainContext { store.dispatch(OnboardingSaltPayAction.Update) }
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.TrySetPin -> {
|
||||
try {
|
||||
assertPinValid(action.pin, getState().pinLength)
|
||||
store.dispatch(OnboardingSaltPayAction.SetPin(action.pin))
|
||||
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.CardRegistration))
|
||||
} catch (error: SaltPayActivationError) {
|
||||
SaltPayExceptionHandler.handle(error)
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.Claim -> {
|
||||
val state = getState()
|
||||
handleInProgress = true
|
||||
|
||||
scope.launch {
|
||||
val amountToClaim = getAmountToClaimIfNeeded(state.saltPayManager, state.amountToClaim).guard {
|
||||
onException(SaltPayActivationError.NoFundsToClaim)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val scanResponse = store.state.globalState.scanResponse
|
||||
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
|
||||
val signer = TangemSigner(
|
||||
card = scanResponse!!.card,
|
||||
tangemSdk = tangemSdk,
|
||||
initialMessage = Message(),
|
||||
accessCode = state.accessCode,
|
||||
) { signResponse ->
|
||||
store.dispatch(
|
||||
GlobalAction.UpdateWalletSignedHashes(
|
||||
walletSignedHashes = signResponse.totalSignedHashes,
|
||||
walletPublicKey = state.saltPayManager.walletPublicKey,
|
||||
remainingSignatures = signResponse.remainingSignatures,
|
||||
),
|
||||
)
|
||||
}
|
||||
state.saltPayManager.claim(amountToClaim.value!!, signer).successOr {
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Claim))
|
||||
onException(it.error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimInProgress))
|
||||
dispatchOnMain(OnboardingSaltPayAction.RefreshClaim)
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.RefreshClaim -> {
|
||||
handleInProgress = true
|
||||
handleClaimRefreshInProgress = true
|
||||
|
||||
val state = getState()
|
||||
scope.launch {
|
||||
val balance = state.saltPayManager.getTokenAmount().successOr {
|
||||
SaltPayExceptionHandler.handle(it.error)
|
||||
handleClaimRefreshInProgress = false
|
||||
return@launch
|
||||
}
|
||||
|
||||
handleInProgress = false
|
||||
handleClaimRefreshInProgress = false
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(balance))
|
||||
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Finished))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
/* do nothing, only reduce */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class UpdateResult(
|
||||
val step: SaltPayActivationStep = SaltPayActivationStep.None,
|
||||
val amountToClaim: Amount? = null,
|
||||
)
|
||||
|
||||
suspend fun SaltPayActivationManager.updateActivationStatus(
|
||||
amountToClaim: Amount?,
|
||||
step: SaltPayActivationStep,
|
||||
): Result<UpdateResult> {
|
||||
Timber.d("updateSaltPayStatus")
|
||||
var updateResult = UpdateResult()
|
||||
|
||||
checkGasIfNeeded(this, step).successOr {
|
||||
return Result.Failure(it.error)
|
||||
|
||||
}
|
||||
registerKYCIfNeeded(this, step).successOr {
|
||||
return Result.Failure(it.error)
|
||||
|
||||
}
|
||||
val saltPayStep = checkRegistration(this).successOr {
|
||||
return Result.Failure(it.error)
|
||||
|
||||
}
|
||||
|
||||
val fetchedAmountToClaim = getAmountToClaimIfNeeded(this, amountToClaim)
|
||||
updateResult = when {
|
||||
// if fetchedAmountToClaim = null -> then it already claimed
|
||||
saltPayStep == SaltPayActivationStep.Claim && fetchedAmountToClaim == null -> {
|
||||
updateResult.copy(
|
||||
step = SaltPayActivationStep.Finished,
|
||||
amountToClaim = fetchedAmountToClaim,
|
||||
)
|
||||
}
|
||||
else -> updateResult.copy(
|
||||
step = saltPayStep,
|
||||
amountToClaim = fetchedAmountToClaim,
|
||||
)
|
||||
}
|
||||
|
||||
Timber.d("updateSaltPayStatus: success: %s", updateResult)
|
||||
return Result.Success(updateResult)
|
||||
}
|
||||
|
||||
private suspend fun getAmountToClaimIfNeeded(
|
||||
saltPayManager: SaltPayActivationManager,
|
||||
amountToClaim: Amount?,
|
||||
): Amount? {
|
||||
Timber.d("getAmountToClaimIfNeeded: for amount: %s", amountToClaim)
|
||||
if (amountToClaim != null) {
|
||||
Timber.d("getAmountToClaimIfNeeded: checks no need, amount already persist")
|
||||
return amountToClaim
|
||||
}
|
||||
|
||||
return when (val result = saltPayManager.getAmountToClaim()) {
|
||||
is Result.Success -> {
|
||||
Timber.d("getAmountToClaimIfNeeded: success: %s", result.data)
|
||||
return result.data
|
||||
}
|
||||
is Result.Failure -> {
|
||||
Timber.e(result.error, "getAmountToClaimIfNeeded: failure")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var handleClaimRefreshInProgress: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
store.dispatchOnMain(OnboardingSaltPayAction.SetClaimRefreshInProgress(value))
|
||||
}
|
||||
|
||||
private var handleInProgress: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
store.dispatchOnMain(OnboardingSaltPayAction.SetInProgress(value))
|
||||
}
|
||||
|
||||
private suspend fun onError(error: SaltPayActivationError) {
|
||||
withMainContext {
|
||||
handleInProgress = false
|
||||
SaltPayExceptionHandler.handle(error)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onException(error: Throwable) {
|
||||
withMainContext {
|
||||
handleInProgress = false
|
||||
SaltPayExceptionHandler.handle(error)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkGasIfNeeded(
|
||||
saltPayManager: SaltPayActivationManager,
|
||||
step: SaltPayActivationStep,
|
||||
): Result<Unit> {
|
||||
Timber.d("checkGasIfNeeded: for step: %s", step)
|
||||
if (step == SaltPayActivationStep.KycStart ||
|
||||
step == SaltPayActivationStep.KycWaiting ||
|
||||
step == SaltPayActivationStep.Finished
|
||||
) {
|
||||
Timber.d("checkGasIfNeeded: no need to check for step: %s", step)
|
||||
return Result.Success(Unit)
|
||||
}
|
||||
|
||||
val result = saltPayManager.checkHasGas()
|
||||
when (result) {
|
||||
is Result.Success -> Timber.d("checkGasIfNeeded: has GAS")
|
||||
is Result.Failure -> Timber.d("checkGasIfNeeded: has NO GAS")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun registerKYCIfNeeded(
|
||||
saltPayManager: SaltPayActivationManager,
|
||||
step: SaltPayActivationStep,
|
||||
): Result<Unit> {
|
||||
Timber.d("registerKYCIfNeeded: step: %s", step)
|
||||
if (step != SaltPayActivationStep.KycStart) {
|
||||
Timber.d("registerKYCIfNeeded: return Success, because step != KycStart")
|
||||
return Result.Success(Unit)
|
||||
}
|
||||
|
||||
withMainContext {
|
||||
Timber.d("registerKYCIfNeeded: set new step: KycWaiting")
|
||||
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycWaiting))
|
||||
}
|
||||
|
||||
Timber.d("saltPayManager.registerKYC()")
|
||||
return saltPayManager.registerKYC()
|
||||
}
|
||||
|
||||
private suspend fun checkRegistration(
|
||||
saltPayManager: SaltPayActivationManager,
|
||||
): Result<SaltPayActivationStep> {
|
||||
Timber.d("checkRegistration")
|
||||
return try {
|
||||
val registrationResponseItem = saltPayManager.checkRegistration().successOr {
|
||||
Timber.e(it.error, "checkRegistration: failure")
|
||||
return it
|
||||
}
|
||||
|
||||
Timber.d("checkRegistration: success: try convert to SaltPayStep")
|
||||
val step = registrationResponseItem.toSaltPayStep()
|
||||
Timber.d("checkRegistration: step: %s", step)
|
||||
Result.Success(step)
|
||||
} catch (ex: SaltPayActivationError) {
|
||||
Timber.e(ex, "checkRegistration: step")
|
||||
Result.Failure(ex)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SaltPayActivationError::class)
|
||||
fun RegistrationResponse.Item.toSaltPayStep(): SaltPayActivationStep {
|
||||
return when {
|
||||
passed != true -> throw SaltPayActivationError.CardNotPassed(this.error)
|
||||
disabledByAdmin == true -> throw SaltPayActivationError.CardDisabled(this.error)
|
||||
|
||||
// go to claim screen
|
||||
active == true -> SaltPayActivationStep.Claim
|
||||
|
||||
// pinSet is false, go toPin screen
|
||||
pinSet == false -> SaltPayActivationStep.NeedPin
|
||||
|
||||
kycStatus != null -> {
|
||||
when (kycStatus) {
|
||||
KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycStart
|
||||
KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting
|
||||
KYCStatus.CORRECTION_REQUESTED, KYCStatus.REJECTED -> SaltPayActivationStep.KycReject
|
||||
KYCStatus.APPROVED -> SaltPayActivationStep.Claim
|
||||
null -> throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
// kycDate is set, go to kyc waiting screen
|
||||
kycDate != null -> SaltPayActivationStep.KycWaiting
|
||||
|
||||
else -> SaltPayActivationStep.KycIntro
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SaltPayActivationError::class)
|
||||
private fun assertPinValid(pin: String, pinLength: Int) {
|
||||
val array = pin.toCharArray()
|
||||
|
||||
if (array.size < pinLength) throw SaltPayActivationError.WeakPin
|
||||
|
||||
val filters: List<Filter<String>> = listOf(
|
||||
AllSymbolsTheSameFilter(),
|
||||
)
|
||||
|
||||
if (filters.any { it.filter(pin) }) {
|
||||
throw SaltPayActivationError.WeakPin
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
|
||||
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class OnboardingSaltPayReducer {
|
||||
companion object {
|
||||
fun reduce(action: Action, state: OnboardingWalletState): OnboardingWalletState {
|
||||
return internalReduce(action, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingWalletState): OnboardingWalletState {
|
||||
val action = anyAction as? OnboardingSaltPayAction ?: return onboardingWalletState
|
||||
|
||||
if (onboardingWalletState.onboardingSaltPayState == null && action is OnboardingSaltPayAction.Init.SetDependencies) {
|
||||
return onboardingWalletState.copy(
|
||||
onboardingSaltPayState = OnboardingSaltPayState(
|
||||
saltPayManager = action.registrationManager,
|
||||
saltPayConfig = action.saltPayConfig,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val oldState = onboardingWalletState.onboardingSaltPayState ?: return onboardingWalletState
|
||||
|
||||
val newState = when (action) {
|
||||
is OnboardingSaltPayAction.SetAccessCode -> {
|
||||
oldState.copy(
|
||||
accessCode = action.accessCode,
|
||||
)
|
||||
}
|
||||
is OnboardingSaltPayAction.SetPin -> {
|
||||
oldState.copy(pinCode = action.pin)
|
||||
}
|
||||
is OnboardingSaltPayAction.SetStep -> {
|
||||
if (action.newStep == null) {
|
||||
oldState
|
||||
} else {
|
||||
oldState.copy(
|
||||
step = action.newStep,
|
||||
)
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.SetAmountToClaim -> {
|
||||
oldState.copy(
|
||||
amountToClaim = action.amount,
|
||||
)
|
||||
}
|
||||
is OnboardingSaltPayAction.SetTokenBalance -> {
|
||||
oldState.copy(
|
||||
tokenAmount = oldState.tokenAmount.copy(value = action.balanceValue),
|
||||
)
|
||||
}
|
||||
is OnboardingSaltPayAction.SetInProgress -> {
|
||||
oldState.copy(
|
||||
inProgress = action.isInProgress,
|
||||
)
|
||||
}
|
||||
is OnboardingSaltPayAction.SetClaimRefreshInProgress -> {
|
||||
oldState.copy(
|
||||
claimInProgress = action.isInProgress,
|
||||
)
|
||||
}
|
||||
else -> oldState
|
||||
}
|
||||
|
||||
return when (newState) {
|
||||
oldState -> onboardingWalletState
|
||||
else -> {
|
||||
onboardingWalletState.copy(onboardingSaltPayState = newState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.domain.common.SaltPayWorkaround
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.network.api.paymentology.PaymentologyApiService
|
||||
import com.tangem.tap.common.toggleWidget.WidgetState
|
||||
import com.tangem.tap.copyToClipboard
|
||||
import com.tangem.tap.domain.extensions.makeSaltPayWalletManager
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.GnosisRegistrator
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.KYCProvider
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.persistence.SaltPayActivationStorage
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.store
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class OnboardingSaltPayState(
|
||||
@Transient
|
||||
val saltPayManager: SaltPayActivationManager = SaltPayActivationManager.stub(),
|
||||
val saltPayConfig: SaltPayConfig = SaltPayConfig.stub(),
|
||||
val pinCode: String? = null,
|
||||
val accessCode: String? = null,
|
||||
val amountToClaim: Amount? = null,
|
||||
val tokenAmount: Amount = Amount(SaltPayWorkaround.tokenFrom(Blockchain.SaltPay), BigDecimal.ZERO),
|
||||
val step: SaltPayActivationStep = SaltPayActivationStep.None,
|
||||
val saltPayCardArtworkUrl: String? = null,
|
||||
val inProgress: Boolean = false,
|
||||
val claimInProgress: Boolean = false,
|
||||
) {
|
||||
|
||||
val mainButtonState: WidgetState
|
||||
get() = if (inProgress) ProgressState.Loading else ProgressState.Done
|
||||
|
||||
fun readyToClaim(): Boolean = amountToClaim != null
|
||||
|
||||
val pinLength: Int = 4
|
||||
|
||||
companion object {
|
||||
fun initDependency(scanResponse: ScanResponse): Pair<SaltPayActivationManager, SaltPayConfig> {
|
||||
val globalState = store.state.globalState
|
||||
val saltPayConfig = globalState.configManager?.config?.saltPayConfig.guard {
|
||||
throw NullPointerException("SaltPayConfig is not initialized")
|
||||
}
|
||||
val gnosisRegistrator = makeGnosisRegistrator(
|
||||
scanResponse = scanResponse,
|
||||
wmFactory = globalState.tapWalletManager.walletManagerFactory,
|
||||
)
|
||||
val registrationManager = makeSaltPayRegistrationManager(
|
||||
scanResponse = scanResponse,
|
||||
gnosisRegistrator = gnosisRegistrator,
|
||||
paymentologyService = store.state.domainNetworks.paymentologyService,
|
||||
registrationStorage = preferencesStorage.saltPayActivationStorage,
|
||||
kycProvider = saltPayConfig.kycProvider,
|
||||
)
|
||||
test(scanResponse, gnosisRegistrator)
|
||||
return registrationManager to saltPayConfig
|
||||
}
|
||||
|
||||
//TODO: delete
|
||||
private fun test(scanResponse: ScanResponse, gnosisRegistrator: GnosisRegistrator) {
|
||||
val map = mapOf(
|
||||
"CID" to scanResponse.card.cardId,
|
||||
"cardPublicKey" to scanResponse.card.cardPublicKey.toHexString(),
|
||||
"walletPublicKey" to gnosisRegistrator.walletManager.wallet.publicKey.blockchainKey.toHexString(),
|
||||
"walletAddress" to gnosisRegistrator.walletManager.wallet.address,
|
||||
)
|
||||
val json = MoshiJsonConverter.INSTANCE.prettyPrint(map)
|
||||
copyToClipboard(json)
|
||||
}
|
||||
|
||||
fun makeSaltPayRegistrationManager(
|
||||
scanResponse: ScanResponse,
|
||||
gnosisRegistrator: GnosisRegistrator,
|
||||
paymentologyService: PaymentologyApiService,
|
||||
registrationStorage: SaltPayActivationStorage,
|
||||
kycProvider: KYCProvider,
|
||||
): SaltPayActivationManager {
|
||||
if (!scanResponse.isSaltPay()) {
|
||||
throw IllegalArgumentException("Can't initialize the OnboardingSaltPayMiddleware if card is not SalPay")
|
||||
}
|
||||
val saltPaySingleWallet = scanResponse.card.wallets.firstOrNull().guard {
|
||||
throw NullPointerException("SaltPay card must have one wallet at least")
|
||||
}
|
||||
|
||||
return SaltPayActivationManager(
|
||||
cardId = scanResponse.card.cardId,
|
||||
cardPublicKey = scanResponse.card.cardPublicKey,
|
||||
walletPublicKey = saltPaySingleWallet.publicKey,
|
||||
kycProvider = kycProvider,
|
||||
paymentologyService = paymentologyService,
|
||||
gnosisRegistrator = gnosisRegistrator,
|
||||
registrationStorage = registrationStorage,
|
||||
)
|
||||
}
|
||||
|
||||
fun makeGnosisRegistrator(
|
||||
scanResponse: ScanResponse,
|
||||
wmFactory: WalletManagerFactory,
|
||||
): GnosisRegistrator {
|
||||
return GnosisRegistrator(
|
||||
walletManager = wmFactory.makeSaltPayWalletManager(scanResponse),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SaltPayActivationStep {
|
||||
None,
|
||||
NoGas,
|
||||
NeedPin,
|
||||
CardRegistration,
|
||||
KycIntro,
|
||||
KycStart,
|
||||
KycWaiting,
|
||||
KycReject,
|
||||
Claim,
|
||||
ClaimInProgress,
|
||||
Finished;
|
||||
}
|
||||
|
|
@ -0,0 +1,377 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.ui
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.ProgressBar
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.view.children
|
||||
import androidx.transition.TransitionManager
|
||||
import coil.load
|
||||
import coil.size.Scale
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.tap.common.compose.PinCodeWidget
|
||||
import com.tangem.tap.common.extensions.configureSettings
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.getDrawableCompat
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.setDrawable
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.extensions.stop
|
||||
import com.tangem.tap.common.extensions.toFormattedCurrencyString
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
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.common.toggleWidget.IndeterminateProgressButtonWidget
|
||||
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
|
||||
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebChromeClient
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebViewClient
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayActivationStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.LayoutOnboardingContainerBottomBinding
|
||||
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
|
||||
import com.tangem.wallet.databinding.LayoutOnboardingMainBinding
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class OnboardingSaltPayView(
|
||||
private val walletFragment: OnboardingWalletFragment,
|
||||
) {
|
||||
|
||||
private val toolbar: MaterialToolbar by lazy { walletFragment.binding.toolbar }
|
||||
private val claimBinding: LayoutOnboardingMainBinding by lazy { walletFragment.bindingSaltPay.claim }
|
||||
private val topContainer: LayoutOnboardingContainerTopBinding by lazy { claimBinding.onboardingTopContainer }
|
||||
private val actionContainer: LayoutOnboardingContainerBottomBinding by lazy { claimBinding.onboardingActionContainer }
|
||||
|
||||
private val btnRefreshBalanceWidget by lazy {
|
||||
RefreshBalanceWidget(claimBinding.onboardingTopContainer.onboardingWalletContainer)
|
||||
}
|
||||
|
||||
private var progressButton: SaltPayProgressButton? = null
|
||||
private var previousStep: SaltPayActivationStep = SaltPayActivationStep.None
|
||||
|
||||
fun newState(state: OnboardingWalletState) {
|
||||
handleCardArtworks(state)
|
||||
|
||||
when (state.step) {
|
||||
OnboardingWalletStep.SaltPay -> setSaltPayStep(state.onboardingSaltPayState)
|
||||
OnboardingWalletStep.Backup -> {
|
||||
if (state.backupState.backupStep == BackupStep.Finished) {
|
||||
// if backup finished -> switch OnboardingWalletStep to the SaltPay
|
||||
store.dispatch(OnboardingWalletAction.GetToSaltPayStep)
|
||||
} else {
|
||||
// if not -> back to the standard backup process
|
||||
walletFragment.handleOnboardingStep(state)
|
||||
}
|
||||
}
|
||||
else -> walletFragment.handleOnboardingStep(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCardArtworks(state: OnboardingWalletState) = with(walletFragment.binding) {
|
||||
//TODO: SaltPay: remove hardCode
|
||||
if (state.onboardingSaltPayState?.saltPayCardArtworkUrl == null) {
|
||||
imvFrontCard.load(R.drawable.img_salt_pay_visa) {
|
||||
scale(Scale.FILL)
|
||||
crossfade(enable = true)
|
||||
}
|
||||
} else {
|
||||
walletFragment.loadImageIntoImageView(state.onboardingSaltPayState.saltPayCardArtworkUrl, imvFrontCard)
|
||||
}
|
||||
imvFirstBackupCard.load(R.drawable.card_placeholder_wallet)
|
||||
|
||||
// if (state.onboardingSaltPayState?.saltPayCardArtworkUrl == null) {
|
||||
// //if saltPay url not loaded -> load from resource
|
||||
// val bitmap = BitmapFactory.decodeResource(walletFragment.resources, R.drawable.img_salt_pay_visa)
|
||||
// imvFrontCard.setImageBitmap(bitmap)
|
||||
// } else {
|
||||
// walletFragment.loadImageIntoImageView(state.onboardingSaltPayState.saltPayCardArtworkUrl, imvFrontCard)
|
||||
// }
|
||||
// walletFragment.loadImageIntoImageView(state.cardArtworkUrl, imvFirstBackupCard)
|
||||
}
|
||||
|
||||
fun showAddBackupCards(state: BackupState) = with(walletFragment.binding) {
|
||||
when (state.backupCardsNumber) {
|
||||
0 -> {
|
||||
tvHeader.text = walletFragment.getText(R.string.onboarding_saltpay_title_no_backup_card)
|
||||
tvBody.text = walletFragment.getText(R.string.onboarding_saltpay_subtitle_no_backup_cards)
|
||||
}
|
||||
1 -> {
|
||||
tvHeader.text = walletFragment.getText(R.string.onboarding_saltpay_title_one_backup_card)
|
||||
tvBody.text = walletFragment.getText(R.string.onboarding_saltpay_subtitle_one_backup_card)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSaltPayStep(onboardingSaltPayState: OnboardingSaltPayState?) {
|
||||
val state = onboardingSaltPayState.guard {
|
||||
store.dispatchDebugErrorNotification("SaltPayView: setSaltPayStep: onboardingSaltPayState = null")
|
||||
return
|
||||
}
|
||||
|
||||
walletFragment.binding.onboardingWalletContainer.hide()
|
||||
walletFragment.bindingSaltPay.onboardingSaltpayContainer.show()
|
||||
|
||||
when (state.step) {
|
||||
SaltPayActivationStep.NoGas -> handleNoGas()
|
||||
SaltPayActivationStep.NeedPin -> handleNeedPin()
|
||||
SaltPayActivationStep.CardRegistration -> handleCardRegistration()
|
||||
SaltPayActivationStep.KycIntro -> handleKycIntro()
|
||||
SaltPayActivationStep.KycStart -> handleKycStart(state)
|
||||
SaltPayActivationStep.KycWaiting -> handleKycWaiting()
|
||||
SaltPayActivationStep.KycReject -> handleKycReject()
|
||||
SaltPayActivationStep.Claim -> handleClaim(state)
|
||||
SaltPayActivationStep.ClaimInProgress -> handleClaim(state)
|
||||
SaltPayActivationStep.Finished -> handleClaim(state)
|
||||
}
|
||||
progressButton?.changeState(state.mainButtonState)
|
||||
}
|
||||
|
||||
private fun handleNoGas() {
|
||||
// normally this shouldn't happen
|
||||
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
store.dispatchDialogShow(SaltPayDialog.Activation.NoGas)
|
||||
}
|
||||
|
||||
private fun handleNeedPin() = with(walletFragment.bindingSaltPay) {
|
||||
toolbar.title = getString(R.string.onboarding_navbar_pin)
|
||||
showOnlyView(pinCode.root) {
|
||||
progressButton = SaltPayProgressButton(pinCode.root)
|
||||
progressButton?.mainView?.isEnabled = false
|
||||
|
||||
var enteredPinCode = ""
|
||||
pinCode.flPinCodeContainer.setContent {
|
||||
PinCodeWidget { code, isLastSymbolEntered ->
|
||||
enteredPinCode = code
|
||||
progressButton?.mainView?.isEnabled = isLastSymbolEntered
|
||||
}
|
||||
}
|
||||
pinCode.btnSetPin.setOnClickListener {
|
||||
store.dispatch(OnboardingSaltPayAction.TrySetPin(enteredPinCode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCardRegistration() = with(walletFragment.bindingSaltPay) {
|
||||
toolbar.title = getString(R.string.onboarding_navbar_register_wallet)
|
||||
showOnlyView(connectCard.root) {
|
||||
progressButton = SaltPayProgressButton(connectCard.root)
|
||||
connectCard.btnConnect.setOnClickListener {
|
||||
store.dispatch(OnboardingSaltPayAction.RegisterCard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleKycIntro() = with(walletFragment.bindingSaltPay) {
|
||||
toolbar.title = getString(R.string.onboarding_navbar_kyc_start)
|
||||
showOnlyView(verifyIdentity.root) {
|
||||
progressButton = SaltPayProgressButton(verifyIdentity.root)
|
||||
verifyIdentity.btnVerify.setOnClickListener {
|
||||
store.dispatch(OnboardingSaltPayAction.OpenUtorgKYC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleKycStart(state: OnboardingSaltPayState) = with(walletFragment.bindingSaltPay) {
|
||||
toolbar.title = getString(R.string.onboarding_navbar_kyc_start)
|
||||
showOnlyView(webvVerifyIdentity) {
|
||||
progressButton = null
|
||||
webvVerifyIdentity.configureSettings()
|
||||
webvVerifyIdentity.webViewClient = UtorgWebViewClient(
|
||||
successUrl = state.saltPayManager.kycUrlProvider.doneUrl,
|
||||
onSuccess = {
|
||||
webvVerifyIdentity.stop()
|
||||
store.dispatch(OnboardingSaltPayAction.UtorgKYCRedirectSuccess)
|
||||
},
|
||||
)
|
||||
webvVerifyIdentity.webChromeClient = UtorgWebChromeClient(walletFragment.requireActivity())
|
||||
webvVerifyIdentity.loadUrl(state.saltPayManager.kycUrlProvider.requestUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleKycWaiting() = with(walletFragment.bindingSaltPay.kycInProgress) {
|
||||
toolbar.title = getString(R.string.onboarding_navbar_kyc_progress)
|
||||
showOnlyView(root)
|
||||
|
||||
imvInProgress.setDrawable(R.drawable.ic_in_progress)
|
||||
tvHeader.text = getString(R.string.onboarding_title_kyc_waiting)
|
||||
tvBody.text = getString(R.string.onboarding_subtitle_kyc_waiting)
|
||||
|
||||
btnOpenSupportChat.hide()
|
||||
btnOpenSupportChat.setOnClickListener {
|
||||
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
|
||||
}
|
||||
|
||||
btnKycAction.text = getString(R.string.onboarding_button_kyc_waiting)
|
||||
btnKycAction.setOnClickListener {
|
||||
store.dispatch(OnboardingSaltPayAction.Update)
|
||||
}
|
||||
progressButton = SaltPayProgressButton(root)
|
||||
}
|
||||
|
||||
private fun handleKycReject() = with(walletFragment.bindingSaltPay.kycInProgress) {
|
||||
toolbar.title = getString(R.string.onboarding_navbar_kyc_progress)
|
||||
showOnlyView(root)
|
||||
|
||||
imvInProgress.setDrawable(R.drawable.ic_reject)
|
||||
tvHeader.text = getString(R.string.onboarding_title_kyc_retry)
|
||||
tvBody.text = getString(R.string.onboarding_subtitle_kyc_retry)
|
||||
|
||||
btnOpenSupportChat.hide()
|
||||
btnKycAction.text = getString(R.string.onboarding_button_kyc_start)
|
||||
btnKycAction.setOnClickListener {
|
||||
store.dispatch(OnboardingSaltPayAction.Update)
|
||||
}
|
||||
progressButton = SaltPayProgressButton(root)
|
||||
}
|
||||
|
||||
private fun handleClaim(state: OnboardingSaltPayState) = with(walletFragment.bindingSaltPay) {
|
||||
toolbar.title = getString(R.string.onboarding_navbar_claim)
|
||||
val btnMain = actionContainer.btnContainer.findViewById<MaterialButton>(R.id.btn_main_action)
|
||||
val tvHeader = actionContainer.tvHeader
|
||||
val tvBody = actionContainer.tvBody
|
||||
|
||||
showOnlyView(claimBinding.root) {
|
||||
// topContainer.progress.pbState.hide()
|
||||
progressButton = SaltPayProgressButton(actionContainer.root)
|
||||
topContainer.imvCardBackground.apply {
|
||||
setBackgroundDrawable(context.getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
|
||||
}
|
||||
topContainer.imvFrontCard.load(R.drawable.img_salt_pay_visa) {
|
||||
scale(Scale.FILL)
|
||||
crossfade(enable = true)
|
||||
}
|
||||
actionContainer.btnAlternativeAction.hide()
|
||||
updateConstraints(state.step, R.layout.lp_onboarding_topup_wallet)
|
||||
}
|
||||
|
||||
btnRefreshBalanceWidget.changeState(if (state.claimInProgress) ProgressState.Loading else ProgressState.Done)
|
||||
if (btnRefreshBalanceWidget.isShowing != true) {
|
||||
btnRefreshBalanceWidget.mainView.setOnClickListener {
|
||||
store.dispatch(OnboardingSaltPayAction.RefreshClaim)
|
||||
}
|
||||
}
|
||||
val tokenAmountString = tokenAmountString(state)
|
||||
topContainer.onboardingTvBalance.tvBalanceValue.text = tokenAmountString ?: ""
|
||||
topContainer.onboardingTvBalance.tvBalanceCurrency.text = ""
|
||||
|
||||
when (state.step) {
|
||||
SaltPayActivationStep.Claim -> {
|
||||
btnRefreshBalanceWidget.mainView.hide()
|
||||
claimValueString(state)?.let {
|
||||
tvHeader.text = getString(R.string.onboarding_title_claim, it)
|
||||
}
|
||||
tvBody.text = getString(R.string.onboarding_subtitle_claim)
|
||||
|
||||
btnMain.text = getString(R.string.onboarding_button_claim)
|
||||
btnMain.setOnClickListener {
|
||||
store.dispatch(OnboardingSaltPayAction.Claim)
|
||||
}
|
||||
}
|
||||
SaltPayActivationStep.ClaimInProgress -> {
|
||||
btnRefreshBalanceWidget.mainView.show()
|
||||
tvHeader.text = getString(R.string.onboarding_title_claim_progress)
|
||||
tvBody.text = getString(R.string.onboarding_subtitle_claim_progress)
|
||||
btnMain.text = getString(R.string.onboarding_button_claim)
|
||||
}
|
||||
SaltPayActivationStep.Finished -> {
|
||||
tvHeader.setText(R.string.common_success)
|
||||
tvBody.setText(R.string.onboarding_subtitle_success_claim)
|
||||
|
||||
btnMain.setText(R.string.onboarding_button_continue_wallet)
|
||||
btnMain.setOnClickListener {
|
||||
walletFragment.showConfetti(false)
|
||||
store.dispatch(OnboardingWalletAction.FinishOnboarding)
|
||||
}
|
||||
|
||||
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
|
||||
updateConstraints(state.step, R.layout.lp_onboarding_done_activation)
|
||||
walletFragment.showConfetti(true)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
// need to recreate after any step
|
||||
progressButton = SaltPayProgressButton(actionContainer.root)
|
||||
}
|
||||
|
||||
private fun tokenAmountString(state: OnboardingSaltPayState): String? {
|
||||
return state.tokenAmount.value?.toFormattedCurrencyString(
|
||||
decimals = state.tokenAmount.decimals,
|
||||
currency = state.tokenAmount.currencySymbol,
|
||||
)
|
||||
}
|
||||
|
||||
private fun claimValueString(state: OnboardingSaltPayState): String? {
|
||||
return state.amountToClaim?.value?.toFormattedCurrencyString(
|
||||
decimals = state.amountToClaim.decimals,
|
||||
currency = state.amountToClaim.currencySymbol,
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateConstraints(currentStep: SaltPayActivationStep, @LayoutRes layoutId: Int) {
|
||||
if (this.previousStep == currentStep) return
|
||||
|
||||
with(claimBinding.onboardingTopContainer) {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(root.context, layoutId)
|
||||
constraintSet.applyTo(onboardingWalletContainer)
|
||||
val transition = InternalNoteLayoutTransition()
|
||||
transition.interpolator = OvershootInterpolator()
|
||||
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showOnlyView(view: View, onShowListener: (() -> Unit)? = null) {
|
||||
walletFragment.bindingSaltPay.onboardingSaltpayContainer.children
|
||||
.filter { it.id != view.id }
|
||||
.forEach { it.hide() }
|
||||
view.show { onShowListener?.invoke() }
|
||||
}
|
||||
|
||||
private fun getString(@StringRes resId: Int): String {
|
||||
return walletFragment.getString(resId)
|
||||
}
|
||||
|
||||
private fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String {
|
||||
return walletFragment.getString(resId, *formatArgs)
|
||||
}
|
||||
|
||||
private class SaltPayProgressButton(
|
||||
root: ViewGroup,
|
||||
) : IndeterminateProgressButtonWidget(findButton(root), findProgress(root)) {
|
||||
companion object {
|
||||
fun findButton(view: ViewGroup): MaterialButton {
|
||||
return findContainer(view).children.firstOrNull { it is MaterialButton } as MaterialButton
|
||||
}
|
||||
|
||||
fun findProgress(view: ViewGroup): View {
|
||||
return findContainer(view).children.firstOrNull { it is ProgressBar }!!
|
||||
}
|
||||
|
||||
fun findContainer(view: ViewGroup): ViewGroup {
|
||||
return view.children.first { it.id == R.id.btn_container } as ViewGroup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.ui
|
||||
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.postUiDelayBg
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.FIRST_BACKUP
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.ORIGIN
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.SECOND_BACKUP
|
||||
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface BackupAnimator {
|
||||
fun updateBackupState(backupState: BackupState)
|
||||
fun setupCreateWalletState()
|
||||
fun showBackupIntro(state: BackupState)
|
||||
fun showScanOriginCard()
|
||||
fun showAddBackupCards(state: BackupState, backupCardsCounts: Int)
|
||||
fun showWritePrimaryCard(state: BackupState)
|
||||
fun showWriteBackupCard(state: BackupState, backupCard: Int)
|
||||
fun showSuccess(onEnd: () -> Unit)
|
||||
}
|
||||
|
||||
class WalletBackupAnimator(
|
||||
private val cardsWidget: WalletCardsWidget,
|
||||
) : BackupAnimator {
|
||||
|
||||
private val viewState: State = when (cardsWidget.leapfrogWidget.getViewsCount()) {
|
||||
2 -> State.TwoCards
|
||||
3 -> State.TreeCards
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private var currentStep = -1
|
||||
|
||||
override fun updateBackupState(backupState: BackupState) {
|
||||
}
|
||||
|
||||
override fun setupCreateWalletState() {
|
||||
currentStep = STEP_CREATE_WALLET
|
||||
|
||||
cardsWidget.toFolded(false)
|
||||
|
||||
ORIGIN.alpha(1f)
|
||||
FIRST_BACKUP.alpha(1f)
|
||||
SECOND_BACKUP.alpha(1f)
|
||||
}
|
||||
|
||||
override fun showBackupIntro(state: BackupState) {
|
||||
currentStep = STEP_BACKUP_INTRO
|
||||
|
||||
ORIGIN.show()
|
||||
FIRST_BACKUP.show()
|
||||
SECOND_BACKUP.show()
|
||||
|
||||
cardsWidget.toWelcome()
|
||||
}
|
||||
|
||||
override fun showScanOriginCard() {
|
||||
currentStep = STEP_SCAN_ORIGIN_CARD
|
||||
|
||||
cardsWidget.toFolded()
|
||||
}
|
||||
|
||||
override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) {
|
||||
when (backupCardsCounts) {
|
||||
0 -> {
|
||||
when (currentStep) {
|
||||
STEP_CREATE_WALLET -> {
|
||||
cardsWidget.toFolded(false) { cardsWidget.toFan() }
|
||||
|
||||
FIRST_BACKUP.animateAlpha(0.6f, 0)
|
||||
SECOND_BACKUP.animateAlpha(0.2f, 400)
|
||||
}
|
||||
STEP_BACKUP_INTRO -> {
|
||||
cardsWidget.toFolded(duration = 200) { cardsWidget.toFan() }
|
||||
FIRST_BACKUP.animateAlpha(0.6f, 400, 250)
|
||||
SECOND_BACKUP.animateAlpha(0.2f, 400, 250)
|
||||
}
|
||||
STEP_SCAN_ORIGIN_CARD -> {
|
||||
cardsWidget.toFan()
|
||||
FIRST_BACKUP.animateAlpha(0.6f, 400, 250)
|
||||
SECOND_BACKUP.animateAlpha(0.2f, 400, 250)
|
||||
}
|
||||
}
|
||||
}
|
||||
1 -> {
|
||||
cardsWidget.toFan(false)
|
||||
FIRST_BACKUP.alpha(1f)
|
||||
SECOND_BACKUP.alpha(0.2f)
|
||||
}
|
||||
2 -> {
|
||||
cardsWidget.toFan(false)
|
||||
FIRST_BACKUP.alpha(1f)
|
||||
SECOND_BACKUP.alpha(1f)
|
||||
}
|
||||
}
|
||||
|
||||
currentStep = STEP_ADD_BACKUP_CARDS
|
||||
}
|
||||
|
||||
override fun showWritePrimaryCard(state: BackupState) {
|
||||
currentStep = STEP_WRITE_PRIMARY_CARD
|
||||
|
||||
cardsWidget.toLeapfrog(
|
||||
onEndFold = {
|
||||
FIRST_BACKUP.alpha(0.4f)
|
||||
SECOND_BACKUP.alpha(0.2f)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun showWriteBackupCard(state: BackupState, backupCard: Int) {
|
||||
|
||||
when (viewState) {
|
||||
State.TwoCards -> {
|
||||
if (currentStep == STEP_WRITE_BACKUP_CARD) return
|
||||
|
||||
val delay = if (currentStep == UNDEFINED) 700L else 0
|
||||
postUiDelayBg(delay) {
|
||||
FIRST_BACKUP.alpha(1f)
|
||||
cardsWidget.leapfrogWidget.leap {
|
||||
ORIGIN.alpha(0.4f)
|
||||
}
|
||||
}
|
||||
}
|
||||
State.TreeCards -> {
|
||||
when (backupCard) {
|
||||
1 -> {
|
||||
FIRST_BACKUP.alpha(1f)
|
||||
cardsWidget.leapfrogWidget.leap {
|
||||
SECOND_BACKUP.alpha(0.4f)
|
||||
ORIGIN.alpha(0.2f)
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
SECOND_BACKUP.alpha(1f)
|
||||
cardsWidget.leapfrogWidget.leap {
|
||||
ORIGIN.alpha(0.4f)
|
||||
FIRST_BACKUP.alpha(0.2f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
currentStep = STEP_WRITE_BACKUP_CARD
|
||||
}
|
||||
|
||||
override fun showSuccess(onEnd: () -> Unit) {
|
||||
currentStep = STEP_SUCCESS
|
||||
cardsWidget.leapfrogWidget.fold(onEnd = onEnd)
|
||||
}
|
||||
|
||||
private enum class State {
|
||||
TreeCards,
|
||||
TwoCards
|
||||
}
|
||||
|
||||
private fun BackupCardType.animateAlpha(
|
||||
alpha: Float,
|
||||
duration: Long = 0L,
|
||||
startDelay: Long = 0L,
|
||||
) {
|
||||
when (this) {
|
||||
ORIGIN -> {
|
||||
cardsWidget.getOriginCardView().animate().apply {
|
||||
alpha(alpha)
|
||||
this.startDelay = startDelay
|
||||
this.duration = duration
|
||||
}
|
||||
}
|
||||
FIRST_BACKUP -> {
|
||||
cardsWidget.getFirstBackupCardView().animate().apply {
|
||||
alpha(alpha)
|
||||
this.startDelay = startDelay
|
||||
this.duration = duration
|
||||
}
|
||||
}
|
||||
SECOND_BACKUP -> {
|
||||
if (viewState != State.TreeCards) return
|
||||
cardsWidget.getSecondBackupCardView().animate().apply {
|
||||
alpha(alpha)
|
||||
this.startDelay = startDelay
|
||||
this.duration = duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupCardType.alpha(alpha: Float) {
|
||||
when (this) {
|
||||
ORIGIN -> cardsWidget.getOriginCardView().alpha = alpha
|
||||
FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().alpha = alpha
|
||||
SECOND_BACKUP -> {
|
||||
if (viewState != State.TreeCards) return
|
||||
cardsWidget.getSecondBackupCardView().alpha = alpha
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupCardType.show() {
|
||||
when (this) {
|
||||
ORIGIN -> cardsWidget.getOriginCardView().show()
|
||||
FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().show()
|
||||
SECOND_BACKUP -> {
|
||||
if (viewState != State.TreeCards) return
|
||||
cardsWidget.getSecondBackupCardView().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UNDEFINED = -1
|
||||
private const val STEP_CREATE_WALLET = 0
|
||||
private const val STEP_BACKUP_INTRO = 1
|
||||
private const val STEP_SCAN_ORIGIN_CARD = 2
|
||||
private const val STEP_ADD_BACKUP_CARDS = 3
|
||||
private const val STEP_WRITE_PRIMARY_CARD = 4
|
||||
private const val STEP_WRITE_BACKUP_CARD = 5
|
||||
private const val STEP_SUCCESS = 6
|
||||
}
|
||||
}
|
||||
|
||||
class TestBackupAnimation(
|
||||
private val animator: BackupAnimator,
|
||||
binding: FragmentOnboardingWalletBinding,
|
||||
) : BackupAnimator {
|
||||
|
||||
private lateinit var state: BackupState
|
||||
private var steps = 0
|
||||
|
||||
init {
|
||||
init(binding, this)
|
||||
}
|
||||
|
||||
fun setStep(step: Int, onStepUpdate: (Int) -> Unit = {}) {
|
||||
steps = step
|
||||
when (steps) {
|
||||
0 -> setupCreateWalletState()
|
||||
1 -> showBackupIntro(state)
|
||||
2 -> showScanOriginCard()
|
||||
3 -> showAddBackupCards(state, 0)
|
||||
4 -> showAddBackupCards(state, 1)
|
||||
5 -> showAddBackupCards(state, 2)
|
||||
6 -> showWritePrimaryCard(state)
|
||||
7 -> showWriteBackupCard(state, 1)
|
||||
8 -> showWriteBackupCard(state, 2)
|
||||
else -> setStep(0)
|
||||
}
|
||||
onStepUpdate(steps)
|
||||
}
|
||||
|
||||
fun onNext(onStepUpdate: (Int) -> Unit) {
|
||||
steps++
|
||||
setStep(steps, onStepUpdate)
|
||||
}
|
||||
|
||||
override fun updateBackupState(backupState: BackupState) {
|
||||
this.state = backupState
|
||||
}
|
||||
|
||||
override fun setupCreateWalletState() {
|
||||
animator.setupCreateWalletState()
|
||||
}
|
||||
|
||||
override fun showBackupIntro(state: BackupState) {
|
||||
animator.showBackupIntro(state)
|
||||
}
|
||||
|
||||
override fun showScanOriginCard() {
|
||||
animator.showScanOriginCard()
|
||||
}
|
||||
|
||||
override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) {
|
||||
animator.showAddBackupCards(state, backupCardsCounts)
|
||||
}
|
||||
|
||||
override fun showWritePrimaryCard(state: BackupState) {
|
||||
animator.showWritePrimaryCard(state)
|
||||
}
|
||||
|
||||
override fun showWriteBackupCard(state: BackupState, backupCard: Int) {
|
||||
animator.showWriteBackupCard(state, backupCard)
|
||||
}
|
||||
|
||||
override fun showSuccess(onEnd: () -> Unit) {
|
||||
animator.showSuccess(onEnd)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun init(binding: FragmentOnboardingWalletBinding, animator: TestBackupAnimation) {
|
||||
val button = MaterialButton(binding.onboardingRoot.context).apply { text = "Next" }
|
||||
button.setOnClickListener { animator.onNext { button.text = "$it" } }
|
||||
binding.onboardingRoot.addView(button, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,9 +5,10 @@ import android.util.TypedValue
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.transition.TransitionInflater
|
||||
import androidx.transition.TransitionManager
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
|
|
@ -19,39 +20,42 @@ import com.tangem.common.core.CardIdDisplayFormat
|
|||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.PropertyCalculator
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.inflate
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.features.BaseFragment
|
||||
import com.tangem.tap.features.FragmentOnBackPressedHandler
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.ui.OnboardingSaltPayView
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
|
||||
import com.tangem.wallet.databinding.LayoutOnboardingSaltpayBinding
|
||||
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
||||
class OnboardingWalletFragment : BaseFragment(R.layout.fragment_onboarding_wallet),
|
||||
StoreSubscriber<OnboardingWalletState>, FragmentOnBackPressedHandler {
|
||||
|
||||
private var accessCodeDialog: AccessCodeDialog? = null
|
||||
private val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
|
||||
internal val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind)
|
||||
internal val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer }
|
||||
|
||||
private val onboardingSaltPayView: OnboardingSaltPayView = OnboardingSaltPayView(this)
|
||||
|
||||
private lateinit var cardsWidget: WalletCardsWidget
|
||||
private val binding: FragmentOnboardingWalletBinding by viewBinding(
|
||||
FragmentOnboardingWalletBinding::bind
|
||||
)
|
||||
private val pbBinding: ViewOnboardingProgressBinding by viewBinding(
|
||||
ViewOnboardingProgressBinding::bind)
|
||||
private var accessCodeDialog: AccessCodeDialog? = null
|
||||
|
||||
private lateinit var animator: BackupAnimator
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
postponeEnterTransition()
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
override fun configureTransitions() {
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(R.transition.fade)
|
||||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
||||
|
|
@ -60,33 +64,58 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val typedValue = TypedValue()
|
||||
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
|
||||
val deviceScaleFactor = typedValue.float
|
||||
|
||||
val leapfrogCalculator = PropertyCalculator(
|
||||
yTranslationFactor = 25f * deviceScaleFactor,
|
||||
)
|
||||
val leapfrog = LeapfrogWidget(binding.flCardsContainer, leapfrogCalculator)
|
||||
cardsWidget = WalletCardsWidget(leapfrog, deviceScaleFactor) { 200f * deviceScaleFactor }
|
||||
startPostponedEnterTransition()
|
||||
val scaleFactor = getDeviceScaleFactor()
|
||||
initCardsWidget(createLeapfrogWidget(binding.flCardsContainer, scaleFactor), scaleFactor)
|
||||
|
||||
binding.viewPagerBackupInfo.adapter = BackupInfoAdapter()
|
||||
TabLayoutMediator(
|
||||
binding.tabLayoutBackupInfo,
|
||||
binding.viewPagerBackupInfo
|
||||
binding.viewPagerBackupInfo,
|
||||
) { tab, position ->
|
||||
//Some implementation
|
||||
}.attach()
|
||||
|
||||
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
||||
|
||||
store.dispatch(OnboardingWalletAction.Init)
|
||||
binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
|
||||
|
||||
store.dispatch(OnboardingWalletAction.LoadArtwork)
|
||||
|
||||
addBackPressHandler(this)
|
||||
|
||||
store.dispatch(OnboardingWalletAction.Init)
|
||||
store.dispatch(OnboardingWalletAction.StartSaltPayCardActivation)
|
||||
store.dispatch(OnboardingWalletAction.LoadArtwork)
|
||||
}
|
||||
|
||||
override fun loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider()
|
||||
|
||||
private fun reInitCardsWidget(backupCardsCounts: Int) = with(binding) {
|
||||
val viewBackupCount = flCardsContainer.childCount - 1
|
||||
if (viewBackupCount <= 0) return@with
|
||||
if (viewBackupCount == backupCardsCounts) return@with
|
||||
|
||||
cardsWidget.toFolded(false)
|
||||
|
||||
if (viewBackupCount > backupCardsCounts) {
|
||||
flCardsContainer.removeViewAt(viewBackupCount - 1)
|
||||
} else {
|
||||
flCardsContainer.inflate(R.layout.view_onboarding_card, true)
|
||||
}
|
||||
|
||||
val scaleFactor = getDeviceScaleFactor()
|
||||
initCardsWidget(createLeapfrogWidget(flCardsContainer, scaleFactor), scaleFactor)
|
||||
}
|
||||
|
||||
private fun createLeapfrogWidget(container: FrameLayout, deviceScaleFactor: Float): LeapfrogWidget {
|
||||
val leapfrogCalculator = PropertyCalculator(yTranslationFactor = 25f * deviceScaleFactor)
|
||||
return LeapfrogWidget(container, leapfrogCalculator)
|
||||
}
|
||||
|
||||
private fun initCardsWidget(leapfrogWidget: LeapfrogWidget, deviceScaleFactor: Float, isTest: Boolean = false) {
|
||||
cardsWidget = WalletCardsWidget(leapfrogWidget, deviceScaleFactor) { 200f * deviceScaleFactor }
|
||||
animator = if (isTest) {
|
||||
TestBackupAnimation(WalletBackupAnimator(cardsWidget), binding)
|
||||
} else {
|
||||
WalletBackupAnimator(cardsWidget)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
@ -105,30 +134,25 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
|
||||
override fun newState(state: OnboardingWalletState) {
|
||||
if (activity == null || view == null) return
|
||||
if (state.isSaltPay) reInitCardsWidget(1)
|
||||
|
||||
animator.updateBackupState(state.backupState)
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
|
||||
with(binding) {
|
||||
loadImageIntoImageView(state.cardArtworkUrl, imvFrontCard)
|
||||
loadImageIntoImageView(state.cardArtworkUrl, imvFirstBackupCard)
|
||||
loadImageIntoImageView(state.cardArtworkUrl, imvSecondBackupCard)
|
||||
}
|
||||
|
||||
pbBinding.pbState.max = 6
|
||||
pbBinding.pbState.max = state.getMaxProgress()
|
||||
pbBinding.pbState.progress = state.getProgressStep()
|
||||
|
||||
when (state.step) {
|
||||
OnboardingWalletStep.None -> {
|
||||
}
|
||||
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
|
||||
OnboardingWalletStep.Backup -> setBackupState(state.backupState)
|
||||
OnboardingWalletStep.Done -> {
|
||||
|
||||
}
|
||||
if (state.isSaltPay) {
|
||||
onboardingSaltPayView.newState(state)
|
||||
} else {
|
||||
loadImageIntoImageView(state.cardArtworkUrl, binding.imvFrontCard)
|
||||
loadImageIntoImageView(state.cardArtworkUrl, binding.imvFirstBackupCard)
|
||||
loadImageIntoImageView(state.cardArtworkUrl, binding.imvSecondBackupCard)
|
||||
handleOnboardingStep(state)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadImageIntoImageView(url: String?, view: ImageView) {
|
||||
internal fun loadImageIntoImageView(url: String?, view: ImageView) {
|
||||
view.load(url) {
|
||||
placeholder(R.drawable.card_placeholder_black)
|
||||
error(R.drawable.card_placeholder_black)
|
||||
|
|
@ -136,86 +160,78 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
}
|
||||
}
|
||||
|
||||
internal fun handleOnboardingStep(state: OnboardingWalletState) {
|
||||
when (state.step) {
|
||||
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
|
||||
OnboardingWalletStep.Backup -> setBackupState(state.backupState, state.isSaltPay)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCreateWalletState() = with(binding) {
|
||||
layoutButtonsCommon.btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
|
||||
layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) }
|
||||
layoutButtonsCommon.btnAlternativeAction.hide()
|
||||
layoutButtonsCommon.btnWalletMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
|
||||
layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) }
|
||||
layoutButtonsCommon.btnWalletAlternativeAction.hide()
|
||||
|
||||
toolbar.title = getText(R.string.onboarding_getting_started)
|
||||
|
||||
tvHeader.setText(R.string.onboarding_create_wallet_header)
|
||||
tvBody.setText(R.string.onboarding_create_wallet_body)
|
||||
|
||||
cardsWidget.toFolded(false) { startPostponedEnterTransition() }
|
||||
animator.setupCreateWalletState()
|
||||
}
|
||||
|
||||
|
||||
private fun setBackupState(state: BackupState) {
|
||||
private fun setBackupState(state: BackupState, isSaltPay: Boolean) {
|
||||
when (state.backupStep) {
|
||||
BackupStep.InitBackup -> showBackupIntro(state)
|
||||
BackupStep.InitBackup -> showBackupIntro(state, isSaltPay)
|
||||
BackupStep.ScanOriginCard -> showScanOriginCard()
|
||||
BackupStep.AddBackupCards -> showAddBackupCards(state)
|
||||
BackupStep.AddBackupCards -> showAddBackupCards(state, isSaltPay)
|
||||
BackupStep.SetAccessCode -> showSetAccessCode()
|
||||
BackupStep.EnterAccessCode -> showEnterAccessCode(state)
|
||||
BackupStep.ReenterAccessCode -> showReenterAccessCode(state)
|
||||
is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state)
|
||||
is BackupStep.WriteBackupCard -> showWriteBackupCard(state)
|
||||
is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state, isSaltPay)
|
||||
is BackupStep.WriteBackupCard -> showWriteBackupCard(state, isSaltPay)
|
||||
BackupStep.Finished -> showSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBackupIntro(state: BackupState) = with(binding) {
|
||||
private fun showBackupIntro(state: BackupState, isSaltPay: Boolean) = with(binding) {
|
||||
imvFirstBackupCard.show()
|
||||
imvSecondBackupCard.show()
|
||||
|
||||
cardsWidget.toWelcome()
|
||||
|
||||
tvHeader.hide()
|
||||
tvBody.hide()
|
||||
viewPagerBackupInfo.show()
|
||||
tabLayoutBackupInfo.show()
|
||||
|
||||
with(layoutButtonsCommon) {
|
||||
btnMainAction.text = getText(R.string.onboarding_button_backup_now)
|
||||
btnAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
|
||||
btnAlternativeAction.show(state.canSkipBackup)
|
||||
btnMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
|
||||
btnAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) }
|
||||
}
|
||||
btnWalletMainAction.text = getText(R.string.onboarding_button_backup_now)
|
||||
btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
|
||||
|
||||
startPostponedEnterTransition()
|
||||
btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
|
||||
btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) }
|
||||
btnWalletAlternativeAction.show(state.canSkipBackup && !isSaltPay)
|
||||
}
|
||||
animator.showBackupIntro(state)
|
||||
}
|
||||
|
||||
private fun showScanOriginCard() = with(binding) {
|
||||
prepareBackupView()
|
||||
|
||||
cardsWidget.toFolded()
|
||||
|
||||
tvHeader.text = getText(R.string.onboarding_title_scan_origin_card)
|
||||
tvBody.text = getString(
|
||||
R.string.onboarding_subtitle_scan_origin_card,
|
||||
)
|
||||
|
||||
with(layoutButtonsCommon) {
|
||||
btnMainAction.text = getString(R.string.onboarding_button_scan_origin_card)
|
||||
btnAlternativeAction.hide()
|
||||
btnMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) }
|
||||
btnWalletMainAction.text = getString(R.string.onboarding_button_scan_origin_card)
|
||||
btnWalletAlternativeAction.hide()
|
||||
btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) }
|
||||
}
|
||||
|
||||
animator.showScanOriginCard()
|
||||
}
|
||||
|
||||
private fun prepareBackupView() = with(binding) {
|
||||
toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup)
|
||||
|
||||
tvHeader.show()
|
||||
tvBody.show()
|
||||
viewPagerBackupInfo.hide()
|
||||
tabLayoutBackupInfo.hide()
|
||||
|
||||
imvFirstBackupCard.show()
|
||||
imvSecondBackupCard.show()
|
||||
}
|
||||
|
||||
private fun showAddBackupCards(state: BackupState) = with(binding) {
|
||||
private fun showAddBackupCards(state: BackupState, isSaltPay: Boolean) = with(binding) {
|
||||
prepareBackupView()
|
||||
|
||||
accessCodeDialog?.dismiss()
|
||||
|
|
@ -230,36 +246,43 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
layoutButtonsAddCards.btnAddCard.isEnabled = false
|
||||
}
|
||||
|
||||
when (state.backupCardsNumber) {
|
||||
0 -> {
|
||||
cardsWidget.toFan() {
|
||||
cardsWidget.getFirstBackupCardView().animate().alpha(0.6f).setDuration(200)
|
||||
cardsWidget.getSecondBackupCardView().animate().alpha(0.2f).setDuration(200)
|
||||
}
|
||||
tvHeader.text = getText(R.string.onboarding_title_no_backup_cards)
|
||||
tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards)
|
||||
}
|
||||
1 -> {
|
||||
tvHeader.text = getText(R.string.onboarding_title_one_backup_card)
|
||||
tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card)
|
||||
|
||||
cardsWidget.toFan(false)
|
||||
cardsWidget.getFirstBackupCardView().animate().alpha(1f).setDuration(400)
|
||||
cardsWidget.getSecondBackupCardView().alpha = 0.2f
|
||||
}
|
||||
2 -> {
|
||||
tvHeader.text = getText(R.string.onboarding_title_two_backup_cards)
|
||||
tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards)
|
||||
|
||||
cardsWidget.toFan(false)
|
||||
cardsWidget.getFirstBackupCardView().alpha = 1f
|
||||
cardsWidget.getSecondBackupCardView().animate().alpha(1f).setDuration(400)
|
||||
}
|
||||
}
|
||||
|
||||
layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup)
|
||||
layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) }
|
||||
layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0
|
||||
|
||||
if (isSaltPay) {
|
||||
onboardingSaltPayView.showAddBackupCards(state)
|
||||
} else {
|
||||
when (state.backupCardsNumber) {
|
||||
0 -> {
|
||||
tvHeader.text = getText(R.string.onboarding_title_no_backup_cards)
|
||||
tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards)
|
||||
}
|
||||
1 -> {
|
||||
tvHeader.text = getText(R.string.onboarding_title_one_backup_card)
|
||||
tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card)
|
||||
}
|
||||
2 -> {
|
||||
tvHeader.text = getText(R.string.onboarding_title_two_backup_cards)
|
||||
tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
animator.showAddBackupCards(state, state.backupCardsNumber)
|
||||
}
|
||||
|
||||
private fun prepareBackupView() = with(binding) {
|
||||
toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup)
|
||||
|
||||
tvHeader.show()
|
||||
tvBody.show()
|
||||
viewPagerBackupInfo.hide()
|
||||
tabLayoutBackupInfo.hide()
|
||||
|
||||
imvFirstBackupCard.show()
|
||||
imvSecondBackupCard.show()
|
||||
}
|
||||
|
||||
private fun showSetAccessCode() {
|
||||
|
|
@ -290,80 +313,71 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
accessCodeDialog?.showError(state.accessCodeError)
|
||||
}
|
||||
|
||||
private fun showWritePrimaryCard(state: BackupState) = with(binding) {
|
||||
private fun showWritePrimaryCard(state: BackupState, isSaltPay: Boolean) = with(binding) {
|
||||
accessCodeDialog?.dismiss()
|
||||
|
||||
reInitCardsWidget(state.backupCardsNumber)
|
||||
prepareViewForFinalizeStep()
|
||||
cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2)
|
||||
|
||||
cardsWidget.toLeapfrog() {
|
||||
cardsWidget.getFirstBackupCardView().alpha = 0.6f
|
||||
cardsWidget.getSecondBackupCardView().alpha = 0.2f
|
||||
}
|
||||
|
||||
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
|
||||
tvHeader.text = getText(R.string.onboarding_title_prepare_origin)
|
||||
tvBody.text = getString(
|
||||
R.string.onboarding_subtitle_scan_primary_card_format,
|
||||
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) }
|
||||
)
|
||||
layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_backup_origin)
|
||||
layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
|
||||
if (isSaltPay) {
|
||||
tvHeader.text = getText(R.string.onboarding_saltpay_title_prepare_origin)
|
||||
tvBody.text = getString(R.string.onboarding_twins_interrupt_warning)
|
||||
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_saltpay_button_backup_origin)
|
||||
} else {
|
||||
tvHeader.text = getText(R.string.onboarding_title_prepare_origin)
|
||||
tvBody.text = getString(
|
||||
R.string.onboarding_subtitle_scan_primary_card_format,
|
||||
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) },
|
||||
)
|
||||
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_backup_origin)
|
||||
}
|
||||
layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
|
||||
|
||||
animator.showWritePrimaryCard(state)
|
||||
}
|
||||
|
||||
private fun prepareViewForFinalizeStep() = with(binding) {
|
||||
layoutButtonsAddCards.root.hide()
|
||||
layoutButtonsCommon.root.show()
|
||||
|
||||
imvFirstBackupCard.show()
|
||||
imvSecondBackupCard.show()
|
||||
|
||||
toolbar.title = getText(R.string.onboarding_button_finalize_backup)
|
||||
|
||||
layoutButtonsAddCards.root.hide()
|
||||
layoutButtonsCommon.root.show()
|
||||
layoutButtonsCommon.btnWalletAlternativeAction.hide()
|
||||
|
||||
imvCardBackground.hide()
|
||||
|
||||
cardsWidget.toLeapfrog()
|
||||
|
||||
layoutButtonsCommon.btnAlternativeAction.hide()
|
||||
imvFirstBackupCard.show()
|
||||
imvSecondBackupCard.show()
|
||||
}
|
||||
|
||||
private fun showWriteBackupCard(state: BackupState) = with(binding) {
|
||||
private fun showWriteBackupCard(state: BackupState, isSaltPay: Boolean) = with(binding) {
|
||||
prepareViewForFinalizeStep()
|
||||
|
||||
cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2)
|
||||
|
||||
val cardNumber = (state.backupStep as? BackupStep.WriteBackupCard)?.cardNumber ?: 1
|
||||
when (cardNumber) {
|
||||
1 -> cardsWidget.leapfrogWidget.leap() {
|
||||
cardsWidget.getOriginCardView().alpha = 0.4f
|
||||
cardsWidget.getFirstBackupCardView().alpha = 0.2f
|
||||
cardsWidget.getSecondBackupCardView().alpha = 1f
|
||||
}
|
||||
2 -> cardsWidget.leapfrogWidget.leap() {
|
||||
cardsWidget.getOriginCardView().alpha = 0.4f
|
||||
cardsWidget.getFirstBackupCardView().alpha = 0.2f
|
||||
cardsWidget.getSecondBackupCardView().alpha = 1f
|
||||
}
|
||||
if (isSaltPay) {
|
||||
tvHeader.text = getString(R.string.onboarding_saltpay_title_backup_card)
|
||||
tvBody.text = getString(R.string.onboarding_twins_interrupt_warning)
|
||||
layoutButtonsCommon.btnWalletMainAction.text = getString(R.string.onboarding_saltpay_title_backup_card)
|
||||
} else {
|
||||
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
|
||||
tvHeader.text = getString(R.string.onboarding_title_backup_card_format, cardNumber)
|
||||
tvBody.text = getString(
|
||||
R.string.onboarding_subtitle_scan_backup_card_format,
|
||||
cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]),
|
||||
)
|
||||
layoutButtonsCommon.btnWalletMainAction.text = getString(
|
||||
R.string.onboarding_button_backup_card_format,
|
||||
cardNumber,
|
||||
)
|
||||
}
|
||||
|
||||
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
|
||||
tvHeader.text =
|
||||
getString(R.string.onboarding_title_backup_card_format, cardNumber)
|
||||
tvBody.text = getString(
|
||||
R.string.onboarding_subtitle_scan_backup_card_format,
|
||||
cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1])
|
||||
)
|
||||
layoutButtonsCommon.btnMainAction.text = getString(
|
||||
R.string.onboarding_button_backup_card_format,
|
||||
cardNumber
|
||||
)
|
||||
layoutButtonsCommon.btnMainAction.setOnClickListener {
|
||||
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
|
||||
store.dispatch(BackupAction.WriteBackupCard(cardNumber))
|
||||
}
|
||||
|
||||
animator.showWriteBackupCard(state, cardNumber)
|
||||
}
|
||||
|
||||
private fun showSuccess() = with(binding) {
|
||||
internal fun showSuccess() = with(binding) {
|
||||
toolbar.title = getString(R.string.onboarding_done_header)
|
||||
tvHeader.text = getText(R.string.onboarding_done_header)
|
||||
|
||||
tvHeader.show()
|
||||
|
|
@ -372,29 +386,40 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
tabLayoutBackupInfo.hide()
|
||||
|
||||
tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding)
|
||||
layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_continue_wallet)
|
||||
layoutButtonsCommon.btnAlternativeAction.hide()
|
||||
layoutButtonsCommon.btnMainAction.setOnClickListener {
|
||||
vConfetti.lavConfetti.cancelAnimation()
|
||||
vConfetti.lavConfetti.hide()
|
||||
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet)
|
||||
layoutButtonsCommon.btnWalletAlternativeAction.hide()
|
||||
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
|
||||
showConfetti(false)
|
||||
store.dispatch(OnboardingWalletAction.FinishOnboarding)
|
||||
}
|
||||
|
||||
cardsWidget.leapfrogWidget.fold {
|
||||
animator.showSuccess {
|
||||
flCardsContainer.hide()
|
||||
imvCardBackground.hide()
|
||||
vConfetti.lavConfetti.show()
|
||||
vConfetti.lavConfetti.playAnimation()
|
||||
showConfetti(true)
|
||||
imvSuccess.alpha = 0f
|
||||
imvSuccess.show()
|
||||
imvSuccess.animate()?.alpha(1f)?.duration = 400
|
||||
}
|
||||
}
|
||||
|
||||
imvSuccess.animate()
|
||||
?.alpha(1f)
|
||||
?.setDuration(400)
|
||||
internal fun showConfetti(show: Boolean) = with(binding.vConfetti) {
|
||||
lavConfetti.show(show)
|
||||
|
||||
if (show) {
|
||||
lavConfetti.playAnimation()
|
||||
} else {
|
||||
lavConfetti.cancelAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(OnboardingWalletAction.OnBackPressed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDeviceScaleFactor(): Float {
|
||||
val typedValue = TypedValue()
|
||||
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
|
||||
return typedValue.float
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,82 +16,100 @@ class WalletCardsWidget(
|
|||
val getTopOfAnchorViewForActivateState: () -> Float,
|
||||
) {
|
||||
|
||||
var currentState: WidgetState? = null
|
||||
private val animDuration: Long = 400
|
||||
|
||||
fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) {
|
||||
var currentState: WidgetState? = null
|
||||
private set
|
||||
|
||||
fun toWelcome(
|
||||
animate: Boolean = true,
|
||||
onEnd: () -> Unit = {},
|
||||
) {
|
||||
if (currentState == WidgetState.WELCOME) return
|
||||
|
||||
currentState = WidgetState.WELCOME
|
||||
|
||||
val animator = createAnimator(animate, onEnd)
|
||||
animator.playTogether(
|
||||
createAnimator(BackupCardType.ORIGIN, createWelcomeProperties(BackupCardType.ORIGIN)),
|
||||
createAnimator(BackupCardType.FIRST_BACKUP, createWelcomeProperties(BackupCardType.FIRST_BACKUP)),
|
||||
createAnimator(BackupCardType.SECOND_BACKUP, createWelcomeProperties(BackupCardType.SECOND_BACKUP))
|
||||
)
|
||||
val animator = createAnimator(animate, onEnd = onEnd)
|
||||
animator.playTogether(createAnimators(::createWelcomeProperties))
|
||||
leapfrogWidget.fold { animator.start() }
|
||||
}
|
||||
|
||||
fun toFolded(animate: Boolean = true, onEnd: () -> Unit = {}) {
|
||||
fun toFolded(
|
||||
animate: Boolean = true,
|
||||
duration: Long = animDuration,
|
||||
onEnd: () -> Unit = {},
|
||||
) {
|
||||
if (currentState == WidgetState.FOLDED) return
|
||||
|
||||
currentState = WidgetState.FOLDED
|
||||
|
||||
val animator = createAnimator(animate, onEnd)
|
||||
animator.playTogether(
|
||||
createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)),
|
||||
createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)),
|
||||
createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP))
|
||||
)
|
||||
leapfrogWidget.fold(animate) { animator.start() }
|
||||
val animator = createAnimator(animate, duration) {
|
||||
leapfrogWidget.fold(animate, onEnd)
|
||||
}
|
||||
animator.playTogether(createAnimators(::createLeapfrogProperties))
|
||||
animator.start()
|
||||
}
|
||||
|
||||
fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) {
|
||||
fun toFan(
|
||||
animate: Boolean = true,
|
||||
onEnd: () -> Unit = {},
|
||||
) {
|
||||
if (currentState == WidgetState.FAN) return
|
||||
|
||||
currentState = WidgetState.FAN
|
||||
|
||||
val animator = createAnimator(animate, onEnd)
|
||||
animator.playTogether(
|
||||
createAnimator(BackupCardType.ORIGIN, createFanProperties(BackupCardType.ORIGIN)),
|
||||
createAnimator(BackupCardType.FIRST_BACKUP, createFanProperties(BackupCardType.FIRST_BACKUP)),
|
||||
createAnimator(BackupCardType.SECOND_BACKUP, createFanProperties(BackupCardType.SECOND_BACKUP))
|
||||
)
|
||||
val animator = createAnimator(animate, onEnd = onEnd)
|
||||
animator.playTogether(createAnimators(::createFanProperties))
|
||||
leapfrogWidget.fold { animator.start() }
|
||||
}
|
||||
|
||||
fun toLeapfrog(animate: Boolean = true, onEnd: () -> Unit = {}) {
|
||||
fun toLeapfrog(
|
||||
animate: Boolean = true,
|
||||
onEndFold: () -> Unit = {},
|
||||
onEnd: () -> Unit = {},
|
||||
) {
|
||||
if (currentState == WidgetState.LEAPFROG) return
|
||||
|
||||
currentState = WidgetState.LEAPFROG
|
||||
|
||||
val animator = createAnimator(animate, onEnd)
|
||||
animator.playTogether(
|
||||
createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)),
|
||||
createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)),
|
||||
createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP))
|
||||
)
|
||||
leapfrogWidget.fold {
|
||||
animator.doOnEnd {
|
||||
val onAnimatorEnd = {
|
||||
leapfrogWidget.fold(animate) {
|
||||
leapfrogWidget.initViews()
|
||||
leapfrogWidget.unfold()
|
||||
onEndFold()
|
||||
leapfrogWidget.unfold(animate, onEnd)
|
||||
}
|
||||
animator.start()
|
||||
}
|
||||
val animator = createAnimator(animate, onEnd = onAnimatorEnd)
|
||||
animator.playTogether(createAnimators(::createLeapfrogProperties))
|
||||
animator.start()
|
||||
}
|
||||
|
||||
private fun createAnimator(animate: Boolean, onEnd: () -> Unit): AnimatorSet {
|
||||
private fun createAnimators(propertyFactory: (BackupCardType) -> CardProperties): List<ObjectAnimator> {
|
||||
return getCardTypesTypes().map { createCardAnimator(it, propertyFactory(it)) }
|
||||
}
|
||||
|
||||
private fun getCardTypesTypes(): List<BackupCardType> = when (leapfrogWidget.getViewsCount()) {
|
||||
2 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP)
|
||||
3 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP, BackupCardType.SECOND_BACKUP)
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private fun createAnimator(animate: Boolean, duration: Long = animDuration, onEnd: () -> Unit): AnimatorSet {
|
||||
return AnimatorSet().apply {
|
||||
duration = if (animate) 400 else 0
|
||||
this.duration = if (animate) duration else 0
|
||||
doOnEnd { onEnd() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAnimator(
|
||||
private fun createCardAnimator(
|
||||
cardType: BackupCardType,
|
||||
properties: CardProperties,
|
||||
): ObjectAnimator {
|
||||
val view = getLeapViewByCardNumber(cardType).view
|
||||
val animator = ObjectAnimator.ofPropertyValuesHolder(view,
|
||||
*properties.createValuesHolders().toTypedArray())
|
||||
val view = getLeapViewByCard(cardType).view
|
||||
val animator = ObjectAnimator.ofPropertyValuesHolder(
|
||||
view,
|
||||
*properties.createValuesHolders().toTypedArray(),
|
||||
)
|
||||
view.elevation = properties.elevation
|
||||
return animator
|
||||
}
|
||||
|
|
@ -176,7 +194,7 @@ class WalletCardsWidget(
|
|||
|
||||
private fun createActivateProperties(cardType: BackupCardType): CardProperties {
|
||||
val topOfAnchorView = getTopOfAnchorViewForActivateState()
|
||||
val twinProperties = CardProperties.from(getLeapViewByCardNumber(cardType).state)
|
||||
val twinProperties = CardProperties.from(getLeapViewByCard(cardType).state)
|
||||
return twinProperties.copy(
|
||||
xTranslation = twinProperties.xTranslation,
|
||||
yTranslation = twinProperties.yTranslation - topOfAnchorView,
|
||||
|
|
@ -185,33 +203,47 @@ class WalletCardsWidget(
|
|||
)
|
||||
}
|
||||
|
||||
private fun getLeapViewByCardNumber(cardType: BackupCardType): LeapView {
|
||||
return when (cardType) {
|
||||
BackupCardType.ORIGIN -> leapfrogWidget.getViewByPosition(0)
|
||||
BackupCardType.FIRST_BACKUP -> leapfrogWidget.getViewByPosition(1)
|
||||
BackupCardType.SECOND_BACKUP -> leapfrogWidget.getViewByPosition(2)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(UnsupportedOperationException::class)
|
||||
fun getOriginCardView(): ImageView {
|
||||
return getLeapViewByCardNumber(BackupCardType.ORIGIN).view as ImageView
|
||||
return getLeapViewByCard(BackupCardType.ORIGIN).view as ImageView
|
||||
}
|
||||
|
||||
@Throws(UnsupportedOperationException::class)
|
||||
fun getFirstBackupCardView(): ImageView {
|
||||
return getLeapViewByCardNumber(BackupCardType.FIRST_BACKUP).view as ImageView
|
||||
return getLeapViewByCard(BackupCardType.FIRST_BACKUP).view as ImageView
|
||||
}
|
||||
|
||||
@Throws(UnsupportedOperationException::class)
|
||||
fun getSecondBackupCardView(): ImageView {
|
||||
return getLeapViewByCardNumber(BackupCardType.SECOND_BACKUP).view as ImageView
|
||||
return getLeapViewByCard(BackupCardType.SECOND_BACKUP).view as ImageView
|
||||
}
|
||||
|
||||
@Throws(UnsupportedOperationException::class)
|
||||
private fun getLeapViewByCard(cardType: BackupCardType): LeapView {
|
||||
fun getByIndex(index: Int): LeapView {
|
||||
return leapfrogWidget.getViewByPosition(leapfrogWidget.getViewPositionByIndex(index))
|
||||
}
|
||||
|
||||
return when (leapfrogWidget.getViewsCount()) {
|
||||
2 -> when (cardType) {
|
||||
BackupCardType.ORIGIN -> getByIndex(1)
|
||||
BackupCardType.FIRST_BACKUP -> getByIndex(0)
|
||||
BackupCardType.SECOND_BACKUP -> throw UnsupportedOperationException()
|
||||
}
|
||||
3 -> when (cardType) {
|
||||
BackupCardType.ORIGIN -> getByIndex(2)
|
||||
BackupCardType.FIRST_BACKUP -> getByIndex(1)
|
||||
BackupCardType.SECOND_BACKUP -> getByIndex(0)
|
||||
}
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
enum class WidgetState { WELCOME, FOLDED, FAN, LEAPFROG }
|
||||
}
|
||||
|
||||
|
||||
enum class BackupCardType { ORIGIN, FIRST_BACKUP, SECOND_BACKUP }
|
||||
|
||||
|
||||
private data class CardProperties(
|
||||
val xTranslation: Float = 0f,
|
||||
val yTranslation: Float = 0f,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class ConfirmDiscardingBackupDialog {
|
|||
setTitle(R.string.welcome_interrupted_backup_discard_title)
|
||||
setMessage(R.string.welcome_interrupted_backup_discard_message)
|
||||
setPositiveButton(R.string.welcome_interrupted_backup_discard_resume) { _, _ ->
|
||||
store.dispatch(BackupAction.ResumeBackup)
|
||||
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup)
|
||||
}
|
||||
setNegativeButton(R.string.welcome_interrupted_backup_discard_discard) { _, _ ->
|
||||
store.dispatch(BackupAction.DiscardSavedBackup)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class UnfinishedBackupFoundDialog {
|
|||
setMessage(R.string.welcome_interrupted_backup_alert_message)
|
||||
setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ ->
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
store.dispatch(BackupAction.ResumeBackup)
|
||||
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup)
|
||||
}
|
||||
setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ ->
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.tap.features.onboarding.service
|
||||
|
||||
//class OnboardingWalletService(
|
||||
// override val fromScreen: AppScreen,
|
||||
// override var scanResponse: ScanResponse,
|
||||
// cardInfoStorage: UsedCardsPrefStorage,
|
||||
// walletManagerFactory: WalletManagerFactory,
|
||||
//) : ProductOnboardingService(cardInfoStorage, walletManagerFactory) {
|
||||
//
|
||||
// override fun navigateToScreen(): AppScreen {
|
||||
// return AppScreen.OnboardingWallet
|
||||
// }
|
||||
//}
|
||||
|
|
@ -245,8 +245,7 @@ private fun sendTransaction(
|
|||
}
|
||||
is SimpleResult.Failure -> {
|
||||
store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError(
|
||||
wallet = walletManager.wallet,
|
||||
host = walletManager.currentHost,
|
||||
walletManager = walletManager,
|
||||
amountToSend = amountToSend,
|
||||
feeAmount = feeAmount,
|
||||
destinationAddress = destinationAddress,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,37 @@
|
|||
package com.tangem.tap.features.tokens.addCustomToken.compose
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.BottomSheetScaffold
|
||||
import androidx.compose.material.BottomSheetScaffoldState
|
||||
import androidx.compose.material.BottomSheetState
|
||||
import androidx.compose.material.BottomSheetValue
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ExtendedFloatingActionButton
|
||||
import androidx.compose.material.FabPosition
|
||||
import androidx.compose.material.FloatingActionButtonDefaults
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material.rememberScaffoldState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
|
@ -15,14 +41,23 @@ import androidx.compose.ui.unit.dp
|
|||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.common.form.DataField
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.ContractAddress
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Decimals
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.DerivationPath
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Name
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Network
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Symbol
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.features.addCustomToken.redux.ScreenState
|
||||
import com.tangem.domain.features.addCustomToken.redux.ViewStates
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.compose.*
|
||||
import com.tangem.tap.common.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.common.compose.AddCustomTokenWarning
|
||||
import com.tangem.tap.common.compose.ClosePopupTrigger
|
||||
import com.tangem.tap.common.compose.ComposeDialogManager
|
||||
import com.tangem.tap.common.compose.ToggledRippleTheme
|
||||
import com.tangem.tap.common.compose.keyboardAsState
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCase
|
||||
import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCasesList
|
||||
import com.tangem.wallet.R
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ class TokensMiddleware {
|
|||
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
|
||||
val derivationStyle = scanResponse.card.derivationStyle
|
||||
|
||||
val addActions = currencyList.mapNotNull { currency ->
|
||||
val addActions = currencyList.mapIndexedNotNull { index, currency ->
|
||||
when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val derivationPath = currency.derivationPath?.let { DerivationPath(it) }
|
||||
|
|
@ -293,12 +293,12 @@ class TokensMiddleware {
|
|||
scanResponse = scanResponse,
|
||||
blockchain = currency.blockchain,
|
||||
derivationParams = derivationParams,
|
||||
) ?: return@mapNotNull null
|
||||
) ?: return@mapIndexedNotNull null
|
||||
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
WalletAction.MultiWallet.AddBlockchain(
|
||||
blockchain = blockchainNetwork,
|
||||
walletManager = walletManager,
|
||||
save = true,
|
||||
save = index == currencyList.lastIndex,
|
||||
)
|
||||
}
|
||||
is Currency.Token -> {
|
||||
|
|
@ -309,7 +309,7 @@ class TokensMiddleware {
|
|||
WalletAction.MultiWallet.AddToken(
|
||||
token = currency.token,
|
||||
blockchain = blockchainNetwork,
|
||||
save = true,
|
||||
save = index == currencyList.lastIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,14 @@ import com.tangem.tap.store
|
|||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentAddTokensBinding
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
|
||||
class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
|
||||
StoreSubscriber<TokensState> {
|
||||
|
||||
|
|
@ -123,7 +127,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
|
|||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.popular_tokens, menu)
|
||||
inflater.inflate(R.menu.menu_popular_tokens, menu)
|
||||
|
||||
val addCustomTokenAllowed = store.state.tokensState.allowToAdd
|
||||
menu.findItem(R.id.menu_navigate_add_custom_token).isVisible = addCustomTokenAllowed
|
||||
|
|
|
|||
|
|
@ -59,9 +59,9 @@ sealed interface Currency {
|
|||
fun isToken(): Boolean = this is Token
|
||||
fun toTokenResponse(): TokenResponse {
|
||||
return TokenResponse(
|
||||
id = coinId ?: "",
|
||||
id = coinId,
|
||||
networkId = blockchain.toNetworkId(),
|
||||
derivationPath = derivationPath ?: DERIVATION_PATH_RAW_VALUE,
|
||||
derivationPath = derivationPath,
|
||||
name = currencyName,
|
||||
symbol = currencySymbol,
|
||||
decimals = decimals,
|
||||
|
|
@ -70,7 +70,6 @@ sealed interface Currency {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val DERIVATION_PATH_RAW_VALUE = "m/44/0'/0/0"
|
||||
fun fromBlockchainNetwork(
|
||||
blockchainNetwork: BlockchainNetwork,
|
||||
token: com.tangem.blockchain.common.Token? = null,
|
||||
|
|
@ -111,12 +110,9 @@ sealed interface Currency {
|
|||
)
|
||||
}
|
||||
|
||||
fun fromTokenResponse(tokenResponse: TokenResponse): Currency {
|
||||
val derivationPath = if (tokenResponse.derivationPath == DERIVATION_PATH_RAW_VALUE) {
|
||||
null
|
||||
} else {
|
||||
tokenResponse.derivationPath
|
||||
}
|
||||
fun fromTokenResponse(tokenResponse: TokenResponse): Currency? {
|
||||
val blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenResponse.networkId)
|
||||
?: return null
|
||||
return when {
|
||||
tokenResponse.contractAddress != null -> Token(
|
||||
com.tangem.blockchain.common.Token(
|
||||
|
|
@ -126,12 +122,12 @@ sealed interface Currency {
|
|||
decimals = tokenResponse.decimals,
|
||||
id = tokenResponse.id,
|
||||
),
|
||||
blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenResponse.networkId)!!,
|
||||
derivationPath = derivationPath,
|
||||
blockchain = blockchain,
|
||||
derivationPath = tokenResponse.derivationPath,
|
||||
)
|
||||
else -> Blockchain(
|
||||
blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenResponse.networkId)!!,
|
||||
derivationPath = derivationPath,
|
||||
blockchain = blockchain,
|
||||
derivationPath = tokenResponse.derivationPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ sealed class WalletWarning(
|
|||
) {
|
||||
data class ExistentialDeposit(
|
||||
val currencyName: String,
|
||||
val currencySymbols: String,
|
||||
val existentialDepositString: String,
|
||||
val edStringValueWithSymbol: String,
|
||||
) : WalletWarning(1)
|
||||
|
||||
object TransactionInProgress : WalletWarning(10)
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ sealed class WalletAction : Action {
|
|||
data class NoAccount(
|
||||
val wallet: Wallet,
|
||||
val blockchain: BlockchainNetwork,
|
||||
val amountToCreateAccount: String
|
||||
val amountToCreateAccount: String,
|
||||
) : WalletAction()
|
||||
|
||||
data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction()
|
||||
|
|
@ -47,6 +47,10 @@ sealed class WalletAction : Action {
|
|||
|
||||
data class SetArtworkId(val artworkId: String?) : WalletAction()
|
||||
|
||||
sealed class UserTokens : WalletAction() {
|
||||
object Loading : UserTokens()
|
||||
object Loaded : UserTokens()
|
||||
}
|
||||
|
||||
sealed class MultiWallet : WalletAction() {
|
||||
data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet()
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ data class WalletState(
|
|||
val totalBalance: TotalBalance? = null,
|
||||
val showBackupWarning: Boolean = false,
|
||||
val missingDerivations: List<BlockchainNetwork> = emptyList(),
|
||||
val loadingUserTokens: Boolean = false,
|
||||
) : StateType {
|
||||
|
||||
// if you do not delegate - the application crashes on startup,
|
||||
|
|
@ -192,21 +193,28 @@ data class WalletState(
|
|||
|
||||
fun removeWallet(walletData: WalletData?): WalletState {
|
||||
if (walletData == null) return this
|
||||
return if (walletData.currency is Currency.Blockchain) {
|
||||
val walletStores = wallets.filterNot {
|
||||
it.blockchainNetwork.blockchain == walletData.currency.blockchain
|
||||
&& it.blockchainNetwork.derivationPath == walletData.currency.derivationPath
|
||||
return when (val currency = walletData.currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val walletStores = wallets.filterNot {
|
||||
it.blockchainNetwork.blockchain == currency.blockchain
|
||||
&& it.blockchainNetwork.derivationPath == currency.derivationPath
|
||||
}
|
||||
copy(wallets = walletStores)
|
||||
.updateTotalBalance()
|
||||
.updateProgressState()
|
||||
}
|
||||
is Currency.Token -> {
|
||||
val walletStore = getWalletStore(walletData.currency)
|
||||
val walletDataList = walletStore?.walletsData
|
||||
?.filterNot { it.currency == walletData.currency }
|
||||
?: emptyList()
|
||||
val updatedWalletManager = walletStore?.walletManager?.also { it.removeToken(currency.token) }
|
||||
val updatedWalletStore = walletStore?.copy(
|
||||
walletsData = walletDataList,
|
||||
walletManager = updatedWalletManager,
|
||||
)
|
||||
updateWalletStore(updatedWalletStore)
|
||||
}
|
||||
copy(wallets = walletStores)
|
||||
.updateTotalBalance()
|
||||
.updateProgressState()
|
||||
} else {
|
||||
val walletStore = getWalletStore(walletData.currency)
|
||||
val walletDataList = walletStore?.walletsData
|
||||
?.filterNot { it.currency == walletData.currency }
|
||||
?: emptyList()
|
||||
val updatedWalletStore = walletStore?.copy(walletsData = walletDataList)
|
||||
updateWalletStore(updatedWalletStore)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -352,8 +360,7 @@ data class WalletData(
|
|||
if (existentialDepositString != null) {
|
||||
val warning = WalletWarning.ExistentialDeposit(
|
||||
currencyName = currency.currencyName,
|
||||
currencySymbols = currency.currencySymbol,
|
||||
existentialDepositString = existentialDepositString,
|
||||
edStringValueWithSymbol = "$existentialDepositString ${currency.currencySymbol}",
|
||||
)
|
||||
walletWarnings.add(warning)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,8 +143,8 @@ class MultiWalletMiddleware {
|
|||
is WalletAction.MultiWallet.ShowWalletBackupWarning -> Unit
|
||||
is WalletAction.MultiWallet.BackupWallet -> {
|
||||
store.state.globalState.scanResponse?.let {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
|
||||
store.dispatch(GlobalAction.Onboarding.Start(it, fromHomeScreen = false))
|
||||
}
|
||||
}
|
||||
is WalletAction.MultiWallet.ScanToGetDerivations -> {
|
||||
|
|
|
|||
|
|
@ -135,7 +135,19 @@ class WalletMiddleware {
|
|||
.plus(Currency.Blockchain(wallet.blockchain, wallet.publicKey.derivationPath?.rawPath))
|
||||
}
|
||||
action.coinsList != null -> action.coinsList
|
||||
else -> walletState.walletsData.map { it.currency }
|
||||
else -> {
|
||||
if (walletState.isMultiwalletAllowed) {
|
||||
walletState.walletsData.map { it.currency }
|
||||
} else {
|
||||
val derivationPath = walletState.primaryWallet?.currency?.derivationPath
|
||||
val primaryBlockchain = walletState.primaryBlockchain
|
||||
val primaryToken = walletState.primaryToken
|
||||
listOfNotNull(
|
||||
primaryBlockchain?.let { Currency.Blockchain(it, derivationPath) },
|
||||
primaryToken?.let { Currency.Token(it, primaryBlockchain!!, derivationPath) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val ratesResult = globalState.tapWalletManager.rates.loadFiatRate(
|
||||
currencyId = appCurrencyId,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class MultiWalletReducer {
|
|||
}
|
||||
|
||||
val selectedCurrency = if (!state.isMultiwalletAllowed) {
|
||||
wallets[0].walletsData[0].currency
|
||||
wallets.firstOrNull()?.walletsData?.firstOrNull()?.currency
|
||||
} else {
|
||||
state.selectedCurrency
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.tangem.tap.features.wallet.ui.BalanceStatus
|
|||
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
|
||||
import com.tangem.tap.features.wallet.ui.TokenData
|
||||
import com.tangem.tap.store
|
||||
import java.math.BigDecimal
|
||||
|
||||
class OnWalletLoadedReducer {
|
||||
|
||||
|
|
@ -115,6 +116,8 @@ class OnWalletLoadedReducer {
|
|||
private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState {
|
||||
if (wallet.blockchain != walletState.primaryBlockchain) return walletState
|
||||
|
||||
// val ratesRespository = store.state.globalState.tapWalletManager.ratesRepository
|
||||
// val tokenCurrency =
|
||||
val fiatCurrencyName = store.state.globalState.appCurrency.code
|
||||
val token = wallet.getFirstToken()
|
||||
val tokenData = if (token != null) {
|
||||
|
|
@ -124,10 +127,12 @@ class OnWalletLoadedReducer {
|
|||
val tokenFiatAmount =
|
||||
tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencyName) }
|
||||
TokenData(
|
||||
tokenAmount.value?.toFormattedCurrencyString(
|
||||
token.decimals, token.symbol
|
||||
amount = tokenAmount.value ?: BigDecimal.ZERO,
|
||||
tokenSymbol = tokenAmount.currencySymbol, fiatAmountFormatted = tokenFiatAmount,
|
||||
fiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatValue(tokenFiatRate) },
|
||||
amountFormatted = tokenAmount.value?.toFormattedCurrencyString(
|
||||
token.decimals, token.symbol,
|
||||
) ?: "",
|
||||
tokenAmount.currencySymbol, tokenFiatAmount
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import com.tangem.tap.features.wallet.redux.WalletStore
|
|||
import com.tangem.tap.features.wallet.ui.BalanceStatus
|
||||
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
|
||||
import org.rekotlin.Action
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
class WalletReducer {
|
||||
|
|
@ -314,6 +315,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
|
|||
is WalletAction.AppCurrencyAction -> {
|
||||
newState = appCurrencyReducer.reduce(action, newState)
|
||||
}
|
||||
is WalletAction.UserTokens.Loading -> newState = newState.copy(loadingUserTokens = true)
|
||||
is WalletAction.UserTokens.Loaded -> newState = newState.copy(loadingUserTokens = false)
|
||||
else -> { /* no-op */
|
||||
}
|
||||
}
|
||||
|
|
@ -387,16 +390,11 @@ private fun setNewFiatRate(
|
|||
state = state
|
||||
)
|
||||
} else {
|
||||
val fiatRate = fiatRates.entries.firstOrNull()
|
||||
val currency = fiatRate?.key ?: return state
|
||||
val rate = fiatRate.value ?: return state
|
||||
|
||||
setSingleWalletFiatRate(
|
||||
rate = rate,
|
||||
rateFormatted = rateFormatter(rate),
|
||||
currency = currency,
|
||||
setSingleWalletFiatRates(
|
||||
fiatRates = fiatRates.mapNotNullValues { it.value },
|
||||
rateFormatter = rateFormatter,
|
||||
appCurrency = appCurrency,
|
||||
state = state
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -427,10 +425,10 @@ private fun setMultiWalletFiatRate(
|
|||
walletData.copy(
|
||||
currencyData = currencyData.copy(
|
||||
fiatAmountFormatted = fiatAmountFormatted,
|
||||
fiatAmount = fiatAmount
|
||||
fiatAmount = fiatAmount,
|
||||
),
|
||||
fiatRate = rate,
|
||||
fiatRateString = rateFormatter(rate)
|
||||
fiatRateString = rateFormatter(rate),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -438,15 +436,51 @@ private fun setMultiWalletFiatRate(
|
|||
.updateWalletsData(newWalletsData)
|
||||
}
|
||||
|
||||
private fun setSingleWalletFiatRates(
|
||||
fiatRates: Map<Currency, BigDecimal>,
|
||||
appCurrency: FiatCurrency,
|
||||
rateFormatter: (BigDecimal) -> String,
|
||||
state: WalletState,
|
||||
): WalletState {
|
||||
val blockchainFiatRate = fiatRates.entries.firstOrNull { it.key.isBlockchain() }
|
||||
val tokenFiatRate = fiatRates.entries.firstOrNull { it.key.isToken() }
|
||||
Timber.e("Token Fiat Rate is ${tokenFiatRate ?: "NULL"}")
|
||||
val updatedState = updateStateWithFiatRate(blockchainFiatRate?.toPair(), appCurrency, rateFormatter, state)
|
||||
return updateStateWithFiatRate(tokenFiatRate?.toPair(), appCurrency, rateFormatter, updatedState)
|
||||
}
|
||||
|
||||
private fun updateStateWithFiatRate(
|
||||
fiatRate: Pair<Currency, BigDecimal>?,
|
||||
appCurrency: FiatCurrency,
|
||||
rateFormatter: (BigDecimal) -> String,
|
||||
state: WalletState,
|
||||
): WalletState {
|
||||
return if (fiatRate != null) {
|
||||
val currency = fiatRate.first
|
||||
val rate = fiatRate.second
|
||||
|
||||
setSingleWalletFiatRate(
|
||||
rate = rate,
|
||||
rateFormatted = rateFormatter(rate),
|
||||
currency = currency,
|
||||
appCurrency = appCurrency,
|
||||
state = state,
|
||||
)
|
||||
} else {
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSingleWalletFiatRate(
|
||||
rate: BigDecimal,
|
||||
rateFormatted: String,
|
||||
currency: Currency,
|
||||
appCurrency: FiatCurrency,
|
||||
state: WalletState
|
||||
state: WalletState,
|
||||
): WalletState {
|
||||
val wallet = state.primaryWalletManager?.wallet ?: return state
|
||||
val token = wallet.getFirstToken()
|
||||
Timber.e("Working with currency: ${currency.currencyName}")
|
||||
|
||||
if (currency == state.primaryWallet?.currency) {
|
||||
val fiatAmount = wallet.amounts[AmountType.Coin]?.value
|
||||
|
|
@ -457,23 +491,38 @@ private fun setSingleWalletFiatRate(
|
|||
fiatRateString = rateFormatted
|
||||
)
|
||||
return state.updateWalletData(walletData)
|
||||
|
||||
} else if (currency is Currency.Token && currency.token == token) {
|
||||
Timber.e("Working with token fiat rate")
|
||||
|
||||
val tokenFiatAmount = wallet.getTokenAmount(token)
|
||||
?.value
|
||||
?.toFiatString(rate, appCurrency.code)
|
||||
val tokenAmountFormatted = tokenFiatAmount?.toFiatString(rate, appCurrency.code)
|
||||
|
||||
val tokenData = state.primaryWallet?.currencyData?.token?.copy(
|
||||
fiatAmountFormatted = tokenAmountFormatted,
|
||||
fiatAmount = tokenFiatAmount,
|
||||
fiatRate = rate,
|
||||
fiatRateString = rateFormatted
|
||||
fiatRateString = rateFormatted,
|
||||
)
|
||||
// ?: TokenData(
|
||||
// fiatAmountFormatted = tokenAmountFormatted,
|
||||
// fiatAmount = tokenFiatAmount,
|
||||
// fiatRate = rate,
|
||||
// fiatRateString = rateFormatted,
|
||||
// amount = "",
|
||||
// tokenSymbol = currency.currencySymbol
|
||||
// )
|
||||
|
||||
Timber.e("Token Data is ${tokenData ?: "NULL"}")
|
||||
|
||||
val walletData = state.primaryWallet?.copy(
|
||||
currencyData = state.primaryWallet.currencyData.copy(
|
||||
token = tokenData
|
||||
)
|
||||
token = tokenData,
|
||||
),
|
||||
)
|
||||
val wallets = walletData?.let { listOf(walletData) } ?: emptyList()
|
||||
return state.updateWalletsData(wallets)
|
||||
Timber.e("Wallet Data is ${walletData ?: "NULL"}")
|
||||
return state.updateWalletData(walletData)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
|
@ -34,9 +34,11 @@ data class BalanceWidgetData(
|
|||
)
|
||||
|
||||
data class TokenData(
|
||||
val amount: String,
|
||||
val amountFormatted: String,
|
||||
val amount: BigDecimal? = null,
|
||||
val tokenSymbol: String,
|
||||
val fiatAmount: String? = null,
|
||||
val fiatAmountFormatted: String? = null,
|
||||
val fiatAmount: BigDecimal? = null,
|
||||
val fiatRateString: String? = null,
|
||||
val fiatRate: BigDecimal? = null,
|
||||
)
|
||||
|
|
@ -151,11 +153,11 @@ class BalanceWidget(
|
|||
groupBaseCurrency.show()
|
||||
tvCurrency.text = data.token?.tokenSymbol
|
||||
tvBaseCurrency.text = data.currency
|
||||
tvAmount.text = if (showAmount) data.token?.amount else ""
|
||||
tvAmount.text = if (showAmount) data.token?.amountFormatted else ""
|
||||
tvBaseAmount.text = if (showAmount) data.amountFormatted else ""
|
||||
if (showAmount) {
|
||||
tvFiatAmount.show()
|
||||
tvFiatAmount.text = data.token?.fiatAmount
|
||||
tvFiatAmount.text = data.token?.fiatAmountFormatted
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT
|
|||
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
|
||||
import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter
|
||||
import com.tangem.tap.features.wallet.ui.images.load
|
||||
import com.tangem.tap.features.wallet.ui.test.TestWalletDetails
|
||||
import com.tangem.tap.features.wallet.ui.test.TestWallet
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentWalletDetailsBinding
|
||||
|
|
@ -125,7 +125,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
view?.findViewById<View>(R.id.l_balance)?.let { view ->
|
||||
TestActions.initFor(
|
||||
view = view,
|
||||
actions = TestWalletDetails.solanaRentExemptWarning()
|
||||
actions = TestWallet.solanaRentExemptWarning()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -345,7 +345,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
|
|||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.wallet_details, menu)
|
||||
inflater.inflate(R.menu.menu_wallet_details, menu)
|
||||
}
|
||||
|
||||
private fun TextView.setWarningStatus(mainMessage: Int, error: String? = null) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import com.tangem.tap.domain.statePrinter.printScanResponseState
|
|||
import com.tangem.tap.domain.statePrinter.printWalletState
|
||||
import com.tangem.tap.domain.termsOfUse.CardTou
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import com.tangem.tap.features.wallet.redux.Artwork
|
||||
import com.tangem.tap.features.wallet.redux.ErrorType
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
|
|
@ -42,7 +41,6 @@ import com.tangem.wallet.R
|
|||
import com.tangem.wallet.databinding.FragmentWalletBinding
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
|
||||
class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<WalletState> {
|
||||
|
||||
private lateinit var warningsAdapter: WarningMessagesAdapter
|
||||
|
|
@ -54,11 +52,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
|
||||
}
|
||||
})
|
||||
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)
|
||||
|
|
@ -89,7 +90,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
}
|
||||
setupWarningsRecyclerView()
|
||||
walletView.changeWalletView(this, binding)
|
||||
// addCustomActionOnCard()
|
||||
addCustomActionOnCard()
|
||||
}
|
||||
|
||||
private fun addCustomActionOnCard() {
|
||||
|
|
@ -111,7 +112,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
override fun newState(state: WalletState) {
|
||||
if (activity == null || view == null) return
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
walletView = MultiWalletView()
|
||||
walletView.changeWalletView(this, binding)
|
||||
}
|
||||
!state.isMultiwalletAllowed && walletView !is SingleWalletView -> {
|
||||
!state.isMultiwalletAllowed && !isSaltPay && walletView !is SingleWalletView -> {
|
||||
walletView = SingleWalletView()
|
||||
walletView.changeWalletView(this, binding)
|
||||
}
|
||||
|
|
@ -139,11 +139,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
if (!state.shouldShowDetails) {
|
||||
binding.toolbar.menu.removeItem(R.id.details_menu)
|
||||
} else if (binding.toolbar.menu.findItem(R.id.details_menu) == null) {
|
||||
binding.toolbar.inflateMenu(R.menu.wallet)
|
||||
binding.toolbar.inflateMenu(R.menu.menu_wallet)
|
||||
}
|
||||
|
||||
setupNoInternetHandling(state)
|
||||
setupCardImage(state.cardImage)
|
||||
setupCardImage(state)
|
||||
|
||||
if (!isSaltPay) showWarningsIfPresent(state.mainWarningsList)
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
binding.srlWallet.isRefreshing = false
|
||||
(activity as? MainActivity)?.showSnackbar(
|
||||
text = R.string.wallet_notification_no_internet,
|
||||
buttonTitle = R.string.common_retry
|
||||
buttonTitle = R.string.common_retry,
|
||||
) { store.dispatch(WalletAction.LoadData) }
|
||||
}
|
||||
} else {
|
||||
|
|
@ -176,13 +176,24 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupCardImage(cardImage: Artwork?) {
|
||||
binding.ivCard.load(cardImage?.artworkId) {
|
||||
scale(Scale.FIT)
|
||||
crossfade(enable = true)
|
||||
placeholder(R.drawable.card_placeholder_black)
|
||||
error(R.drawable.card_placeholder_black)
|
||||
fallback(R.drawable.card_placeholder_black)
|
||||
private fun setupCardImage(state: WalletState) {
|
||||
//TODO: SaltPay: remove hardCode
|
||||
if (store.state.globalState.scanResponse?.isSaltPay() == true) {
|
||||
binding.ivCard.load(R.drawable.img_salt_pay_visa) {
|
||||
scale(Scale.FIT)
|
||||
crossfade(enable = true)
|
||||
placeholder(R.drawable.card_placeholder_black)
|
||||
error(R.drawable.card_placeholder_black)
|
||||
fallback(R.drawable.card_placeholder_black)
|
||||
}
|
||||
} else {
|
||||
binding.ivCard.load(state.cardImage?.artworkId) {
|
||||
scale(Scale.FIT)
|
||||
crossfade(enable = true)
|
||||
placeholder(R.drawable.card_placeholder_black)
|
||||
error(R.drawable.card_placeholder_black)
|
||||
fallback(R.drawable.card_placeholder_black)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -191,11 +202,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
R.id.details_menu -> {
|
||||
store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers))
|
||||
store.state.globalState.scanResponse?.let { scanNoteResponse ->
|
||||
store.dispatch(DetailsAction.PrepareScreen(
|
||||
scanNoteResponse,
|
||||
store.state.walletState.walletManagers.map { it.wallet },
|
||||
CardTou(),
|
||||
))
|
||||
store.dispatch(
|
||||
DetailsAction.PrepareScreen(
|
||||
scanNoteResponse,
|
||||
store.state.walletState.walletManagers.map { it.wallet },
|
||||
CardTou(),
|
||||
),
|
||||
)
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
|
||||
true
|
||||
}
|
||||
|
|
@ -206,7 +219,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
|
|||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
if (store.state.walletState.shouldShowDetails) inflater.inflate(R.menu.wallet, menu)
|
||||
if (store.state.walletState.shouldShowDetails) inflater.inflate(R.menu.menu_wallet, menu)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ class WalletWarningConverter(
|
|||
is WalletWarning.ExistentialDeposit -> {
|
||||
context.getString(
|
||||
R.string.warning_existential_deposit_message,
|
||||
message.currencyName, message.currencySymbols, message.existentialDepositString,
|
||||
message.currencyName, message.edStringValueWithSymbol,
|
||||
)
|
||||
}
|
||||
is WalletWarning.BalanceNotEnoughForFee -> {
|
||||
|
|
|
|||
|
|
@ -110,8 +110,12 @@ class WalletAdapter
|
|||
lContent.tvExchangeRate.text = wallet.fiatRateString
|
||||
?: root.getString(id = R.string.token_item_no_rate)
|
||||
|
||||
cardWallet.setOnClickListener {
|
||||
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))
|
||||
if (wallet.walletAddresses != null) {
|
||||
cardWallet.setOnClickListener {
|
||||
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))
|
||||
}
|
||||
} else {
|
||||
cardWallet.setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,23 @@ class SimpleOkDialog {
|
|||
}
|
||||
}.create()
|
||||
}
|
||||
}
|
||||
|
||||
fun create(dialog: AppDialog.SimpleOkErrorDialog, context: Context): AlertDialog = create(
|
||||
dialog = AppDialog.SimpleOkDialog(
|
||||
header = context.getString(R.string.common_error),
|
||||
message = dialog.message,
|
||||
onOk = dialog.onOk,
|
||||
),
|
||||
context = context,
|
||||
)
|
||||
|
||||
fun create(dialog: AppDialog.SimpleOkWarningDialog, context: Context): AlertDialog = create(
|
||||
dialog = AppDialog.SimpleOkDialog(
|
||||
header = context.getString(R.string.common_warning),
|
||||
message = dialog.message,
|
||||
onOk = dialog.onOk,
|
||||
),
|
||||
context = context,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
package com.tangem.tap.features.wallet.ui.test
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.tap.common.TestAction
|
||||
import com.tangem.tap.common.TestActions
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -13,7 +17,7 @@ import kotlin.random.Random
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class TestWalletDetails {
|
||||
class TestWallet {
|
||||
companion object {
|
||||
fun solanaRentExemptWarning(): List<TestAction> {
|
||||
val checker = SolanaRentWarningActionEmitter()
|
||||
|
|
@ -26,6 +30,80 @@ class TestWalletDetails {
|
|||
"BALANCE = 0.00089089" to { checker.setMoreThanRentExemptByOne() },
|
||||
)
|
||||
}
|
||||
|
||||
fun getBlockchainBalanceActions(blockchainNetwork: BlockchainNetwork): List<TestAction> {
|
||||
return getBlockchainBalanceActions(
|
||||
getWalletManager(blockchainNetwork),
|
||||
blockchainNetwork.blockchain.decimals(),
|
||||
)
|
||||
}
|
||||
|
||||
fun getTokenBalanceAction(blockchainNetwork: BlockchainNetwork, token: Token): List<TestAction> {
|
||||
return getTokenBalanceAction(getWalletManager(blockchainNetwork), token)
|
||||
}
|
||||
|
||||
fun getBlockchainBalanceActions(walletManager: WalletManager?, decimals: Int): List<TestAction> {
|
||||
val minValue = BigDecimal.ONE.movePointLeft(decimals)
|
||||
val averageValue = BigDecimal.ONE.movePointRight(decimals)
|
||||
.divide(BigDecimal(2)).movePointLeft(decimals)
|
||||
val maxValue = BigDecimal(2).pow(32)
|
||||
|
||||
return listOf(
|
||||
"0.0" to { setBalance(walletManager, BigDecimal.ZERO) },
|
||||
"Min value" to { setBalance(walletManager, minValue) },
|
||||
"Average value" to { setBalance(walletManager, averageValue) },
|
||||
"Max value" to { setBalance(walletManager, maxValue) },
|
||||
"1234567890123.01234567890123" to { setBalance(walletManager, BigDecimal.ZERO) },
|
||||
)
|
||||
}
|
||||
|
||||
fun getTokenBalanceAction(walletManager: WalletManager?, token: Token): List<TestAction> {
|
||||
val minValue = BigDecimal.ONE.movePointLeft(token.decimals)
|
||||
val averageValue = BigDecimal.ONE.movePointRight(token.decimals)
|
||||
.divide(BigDecimal(2)).movePointLeft(token.decimals)
|
||||
val maxValue = BigDecimal(2).pow(32)
|
||||
|
||||
return listOf(
|
||||
"0.0" to { setTokenBalance(walletManager, BigDecimal.ZERO, token) },
|
||||
"Min value" to { setTokenBalance(walletManager, minValue, token) },
|
||||
"Average value" to { setTokenBalance(walletManager, averageValue, token) },
|
||||
"Max value" to { setTokenBalance(walletManager, maxValue, token) },
|
||||
"1234567890123.01234567890123" to { setTokenBalance(walletManager, BigDecimal.ZERO, token) },
|
||||
)
|
||||
}
|
||||
|
||||
fun setBalance(walletManager: WalletManager?, value: BigDecimal) {
|
||||
val manager = walletManager.guard {
|
||||
store.dispatchDebugErrorNotification("WalletManager not found")
|
||||
return
|
||||
}
|
||||
val amount = manager.wallet.amounts[AmountType.Coin] ?: Amount(manager.wallet.blockchain)
|
||||
setBalance(manager, amount, value)
|
||||
}
|
||||
|
||||
private fun setTokenBalance(walletManager: WalletManager?, value: BigDecimal, token: Token? = null) {
|
||||
val manager = walletManager.guard {
|
||||
store.dispatchDebugErrorNotification("WalletManager not found")
|
||||
return
|
||||
}
|
||||
val token = token.guard {
|
||||
store.dispatchDebugErrorNotification("Token not found")
|
||||
return
|
||||
}
|
||||
|
||||
val amount = manager.wallet.amounts[AmountType.Token(token)] ?: Amount(token)
|
||||
setBalance(manager, amount, value)
|
||||
}
|
||||
|
||||
private fun setBalance(walletManager: WalletManager, amount: Amount, value: BigDecimal) {
|
||||
TestActions.testAmountInjectionForWalletManagerEnabled = true
|
||||
walletManager.wallet.setAmount(amount.copy(value = value))
|
||||
store.dispatch(WalletAction.LoadData)
|
||||
}
|
||||
|
||||
private fun getWalletManager(blockchainNetwork: BlockchainNetwork): WalletManager? {
|
||||
return store.state.walletState.getWalletManager(blockchainNetwork)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ class MultiWalletView : WalletView() {
|
|||
handleRescanWarning(binding, state.missingDerivations.isNotEmpty())
|
||||
walletsAdapter.submitList(state.walletsData, state.primaryBlockchain, state.primaryToken)
|
||||
|
||||
binding.pbLoadingUserTokens.show(state.loadingUserTokens)
|
||||
|
||||
binding.btnAddToken.setOnClickListener {
|
||||
val card = store.state.globalState.scanResponse!!.card
|
||||
store.dispatch(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package com.tangem.tap.features.wallet.ui.wallet
|
||||
|
||||
import com.tangem.domain.common.extensions.debounce
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.animateVisibility
|
||||
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
|
||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.databinding.LayoutSingleWalletBalanceBinding
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class SaltPayBalanceWidgetData(
|
||||
|
|
@ -26,7 +29,7 @@ class SaltPayBalanceWidget(
|
|||
veilBalance.veil()
|
||||
veilBalanceCrypto.veil()
|
||||
} else {
|
||||
veilBalance.unVeil()
|
||||
// veilBalance.unVeil()
|
||||
veilBalanceCrypto.unVeil()
|
||||
}
|
||||
tvProcessing.animateVisibility(
|
||||
|
|
@ -35,9 +38,18 @@ class SaltPayBalanceWidget(
|
|||
veilBalanceCrypto.animateVisibility(
|
||||
show = data.state != ProgressState.Error,
|
||||
)
|
||||
tvBalance.text = data.fiatAmount?.formatAmountAsSpannedString(
|
||||
currencySymbol = data.fiatCurrency?.symbol ?: "",
|
||||
)
|
||||
if (data.fiatAmount == null) {
|
||||
//TODO: SaltPay: A tricky solution to the problem with displaying rates
|
||||
// If the rates are loaded after the walletManager.update() is completely updated,
|
||||
// then this problem can be avoided
|
||||
actionDebouncer(WalletAction.LoadFiatRate())
|
||||
} else {
|
||||
veilBalance.unVeil()
|
||||
tvBalance.text = data.fiatAmount?.formatAmountAsSpannedString(
|
||||
currencySymbol = data.fiatCurrency?.symbol ?: "",
|
||||
)
|
||||
}
|
||||
|
||||
tvBalanceCrypto.text = data.currency
|
||||
|
||||
tvCurrencyName.text = data.fiatCurrency?.code
|
||||
|
|
@ -48,4 +60,5 @@ class SaltPayBalanceWidget(
|
|||
}
|
||||
}
|
||||
|
||||
private val actionDebouncer = debounce<Action>(500, mainScope) { store.dispatch(it) }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ package com.tangem.tap.features.wallet.ui.wallet
|
|||
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.features.wallet.redux.WalletData
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.ui.TokenData
|
||||
import com.tangem.tap.features.wallet.ui.WalletFragment
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.databinding.FragmentWalletBinding
|
||||
import timber.log.Timber
|
||||
|
||||
class SaltPaySingleWalletView : WalletView() {
|
||||
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
|
||||
|
|
@ -18,8 +19,10 @@ class SaltPaySingleWalletView : WalletView() {
|
|||
private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) {
|
||||
rvMultiwallet.hide()
|
||||
btnAddToken.hide()
|
||||
rowButtons.hide()
|
||||
rvPendingTransaction.hide()
|
||||
tvTwinCardNumber.hide()
|
||||
pbLoadingUserTokens.hide()
|
||||
lCardBalance.root.hide()
|
||||
lAddress.root.hide()
|
||||
lSingleWalletBalance.root.show()
|
||||
|
|
@ -30,20 +33,21 @@ class SaltPaySingleWalletView : WalletView() {
|
|||
|
||||
override fun onNewState(state: WalletState) {
|
||||
val binding = binding ?: return
|
||||
state.primaryWallet ?: return
|
||||
val tokenData = state.primaryWallet?.currencyData?.token ?: return
|
||||
|
||||
setupBalance(state, state.primaryWallet, binding)
|
||||
setupBalance(state, tokenData, binding)
|
||||
}
|
||||
|
||||
private fun setupBalance(state: WalletState, primaryWallet: WalletData, binding: FragmentWalletBinding) {
|
||||
private fun setupBalance(state: WalletState, tokenData: TokenData, binding: FragmentWalletBinding) {
|
||||
binding.lSingleWalletBalance.root.show()
|
||||
Timber.e("Current address is ${state.primaryWalletManager?.wallet?.address}")
|
||||
SaltPayBalanceWidget(
|
||||
binding = binding.lSingleWalletBalance,
|
||||
data = SaltPayBalanceWidgetData(
|
||||
state = state.state,
|
||||
currencySymbol = primaryWallet.currencyData.currencySymbol,
|
||||
currency = primaryWallet.currencyData.amountFormatted,
|
||||
fiatAmount = primaryWallet.currencyData.fiatAmount,
|
||||
currencySymbol = tokenData.tokenSymbol,
|
||||
currency = tokenData.amountFormatted,
|
||||
fiatAmount = tokenData.fiatAmount, // TODO: show fiatAmount
|
||||
fiatCurrency = store.state.globalState.appCurrency,
|
||||
),
|
||||
).setup()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.tap.common.extensions.beginDelayedTransition
|
||||
import com.tangem.tap.common.extensions.fitChipsByGroupWidth
|
||||
import com.tangem.tap.common.extensions.getQuantityString
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
|
||||
|
|
@ -36,6 +37,7 @@ class SingleWalletView : WalletView() {
|
|||
rvMultiwallet.hide()
|
||||
btnAddToken.hide()
|
||||
rvPendingTransaction.hide()
|
||||
pbLoadingUserTokens.hide()
|
||||
lCardBalance.root.show()
|
||||
lAddress.root.show()
|
||||
lSingleWalletBalance.root.hide()
|
||||
|
|
@ -85,11 +87,7 @@ class SingleWalletView : WalletView() {
|
|||
private fun setupTwinCards(twinCardsState: TwinCardsState?, binding: FragmentWalletBinding) = with(binding) {
|
||||
twinCardsState?.cardNumber?.let { cardNumber ->
|
||||
tvTwinCardNumber.show()
|
||||
val number = when (cardNumber) {
|
||||
TwinCardNumber.First -> 1
|
||||
TwinCardNumber.Second -> 2
|
||||
}
|
||||
tvTwinCardNumber.text = fragment?.getString(R.string.wallet_twins_chip_format, number, 2)
|
||||
tvTwinCardNumber.text = tvTwinCardNumber.getQuantityString(R.plurals.card_label_card_count, 2)
|
||||
}
|
||||
if (twinCardsState?.cardNumber == null) {
|
||||
tvTwinCardNumber.hide()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue