Updated on 2026-08-14

This commit is contained in:
Tangem 2022-10-29 00:21:24 +05:00
commit b6db9e390a
147 changed files with 3963 additions and 3570 deletions

View file

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

View file

@ -3,8 +3,8 @@ package com.tangem.tap.features.details.redux
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
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.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.AnalyticsParamAnOld
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
@ -110,9 +110,9 @@ class DetailsMiddleware {
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandlers?.logCardSdkError(
store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent(
error,
Analytics.ActionToLog.PurgeWallet,
AnalyticsAnOld.ActionToLog.PurgeWallet,
card = store.state.detailsState.scanResponse?.card,
)
}
@ -154,11 +154,11 @@ class DetailsMiddleware {
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandlers?.logCardSdkError(
store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent(
error = error,
actionToLog = Analytics.ActionToLog.ChangeSecOptions,
parameters = mapOf(
AnalyticsParam.NEW_SECURITY_OPTION to
action = AnalyticsAnOld.ActionToLog.ChangeSecOptions,
params = mapOf(
AnalyticsParamAnOld.NEW_SECURITY_OPTION to
(selectedOption?.name ?: ""),
),
card = store.state.detailsState.scanResponse?.card,

View file

@ -25,6 +25,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
SettingsElement.WalletConnect -> {
if (state.scanResponse?.card?.isMultiwalletAllowed == true) it else null
}
SettingsElement.SendFeedback -> if (state.scanResponse?.card?.isSaltPay != true) it else null
SettingsElement.LinkMoreCards -> {
// if (state.createBackupAllowed) it else null
// TODO: SaltPay: temporary excluding backup process for Visa cards
@ -78,8 +79,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)

View file

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

View file

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

View file

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

View file

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

View file

@ -55,25 +55,25 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer),
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
initAndLoadTOS()
setOnClickListeners()
initViews()
}
private fun initAndLoadTOS() {
private fun initViews() {
binding.webView.configureSettings()
binding.webView.loadUrl("file:///android_asset/tos.html")
}
private fun setOnClickListeners() {
binding.btnAccept.setOnClickListener { store.dispatch(DisclaimerAction.AcceptDisclaimer) }
}
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())
}
}

View file

@ -16,7 +16,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.store
import org.rekotlin.StoreSubscriber
@ -38,7 +37,6 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
): View? {
val context = container?.context ?: return null
store.dispatch(BackupAction.CheckForUnfinishedBackup)
composeView = ComposeView(context).apply {
setContent {
AppCompatTheme {

View file

@ -4,15 +4,14 @@ import com.tangem.tap.common.entities.IndeterminateProgressButton
import org.rekotlin.Action
sealed class HomeAction : Action {
object Init : HomeAction()
data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction()
// 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()
}

View file

@ -1,19 +1,23 @@
package com.tangem.tap.features.home.redux
import com.tangem.common.card.Card
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.operations.backup.BackupService
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.backupService
import com.tangem.tap.common.analytics.AnalyticsEventAnOld
import com.tangem.tap.common.analytics.AnalyticsParamAnOld
import com.tangem.tap.common.analytics.GetCardSourceParamsAnOld
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
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.extensions.primaryCardIsSaltPayVisa
import com.tangem.tap.common.postUiDelayBg
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
@ -21,6 +25,9 @@ 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
@ -28,8 +35,8 @@ 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.dialog.SaltPayDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
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
@ -65,8 +72,8 @@ private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch
is HomeAction.Init -> {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.ExchangeManager.Init)
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
store.dispatch(GlobalAction.FetchUserCountry)
store.dispatch(BackupAction.CheckForUnfinishedBackup)
}
is HomeAction.ShouldScanCardOnResume -> {
if (action.shouldScanCard) {
@ -75,43 +82,97 @@ private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch
}
}
is HomeAction.ReadCard -> {
if (!preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
changeButtonState(ButtonState.PROGRESS)
val scanCardAction = GlobalAction.ScanCard(
onSuccess = ::onScanSuccess,
onFailure = ::onScanFailure,
)
store.dispatch(HomeAction.ScanInProgress(true))
postUiDelayBg(300) { store.dispatch(scanCardAction) }
}
changeButtonState(ButtonState.PROGRESS)
val scanCardAction = GlobalAction.ScanCard(
onSuccess = { scanResponse ->
store.dispatch(HomeAction.ScanInProgress(false))
checkForUnfinishedBackupForSaltPay(
backupService = backupService,
scanResponse = scanResponse,
nextHandler = {
showDisclaimerIfNeed(
scanResponse = scanResponse,
nextHandler = ::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),
store.state.globalState.analyticsHandler?.handleAnalyticsEvent(
event = AnalyticsEventAnOld.GET_CARD,
params = mapOf(AnalyticsParamAnOld.SOURCE.param to GetCardSourceParamsAnOld.WELCOME.param),
)
}
}
}
/**
* It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards
* see BackupAction.CheckForUnfinishedBackup
* If user touches card other than Visa SaltPay - show dialog and block next processing
*/
private fun checkForUnfinishedBackupForSaltPay(
backupService: BackupService,
scanResponse: ScanResponse,
nextHandler: (ScanResponse) -> Unit,
) {
if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
nextHandler(scanResponse)
return
}
fun isTheSamePrimaryCard(card: Card): Boolean {
return backupService.primaryCardId?.let { it == card.cardId } ?: false
}
if (scanResponse.isSaltPayWallet() || !isTheSamePrimaryCard(scanResponse.card)) {
changeButtonState(ButtonState.ENABLED)
showSaltPayTapVisaLogoCardDialog()
} else {
nextHandler(scanResponse)
}
}
private fun showDisclaimerIfNeed(scanResponse: ScanResponse, nextHandler: (ScanResponse) -> Unit) {
val disclaimerType = DisclaimerType.get(scanResponse)
store.dispatch(DisclaimerAction.SetDisclaimerType(disclaimerType))
if (disclaimerType.isAccepted()) {
nextHandler((scanResponse))
} else {
changeButtonState(ButtonState.ENABLED)
store.dispatch(
DisclaimerAction.Show {
changeButtonState(ButtonState.PROGRESS)
nextHandler(scanResponse)
},
)
}
}
private fun onScanSuccess(scanResponse: ScanResponse) {
val globalState = store.state.globalState
val tapWalletManager = globalState.tapWalletManager
tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(HomeAction.ScanInProgress(false))
globalState.analyticsHandler.attachToAllEvents(AnalyticsParam.BatchId, scanResponse.card.batchId)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (scanResponse.isSaltPay()) {
if (scanResponse.isSaltPayVisa()) {
scope.launch {
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse)
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager)
delay(500)
withMainContext {
when (result) {
@ -119,10 +180,8 @@ private fun onScanSuccess(scanResponse: ScanResponse) {
val isOnboardingCase = result.data
if (isOnboardingCase) {
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
store.dispatch(OnboardingSaltPayAction.Init.SetDependencies(manager, config))
store.dispatch(OnboardingSaltPayAction.Update)
// store.dispatch(OnboardingSaltPayAction.Init.DiscardBackupSteps)
navigateTo(AppScreen.OnboardingWallet)
} else {
navigateTo(AppScreen.Wallet)
@ -131,20 +190,7 @@ private fun onScanSuccess(scanResponse: ScanResponse) {
}
is Result.Failure -> {
changeButtonState(ButtonState.ENABLED)
when (val error = result.error) {
is SaltPayRegistrationError -> {
val dialog = when (error) {
is SaltPayRegistrationError.NoGas -> SaltPayDialog.NoFundsForActivation
else -> SaltPayDialog.RegistrationError(error)
}
store.dispatchDialogShow(dialog)
}
else -> {
store.dispatchDebugErrorNotification(
error.localizedMessage ?: "SaltPay: Unknown error",
)
}
}
SaltPayExceptionHandler.handle(result.error)
}
}
}
@ -152,12 +198,7 @@ private fun onScanSuccess(scanResponse: ScanResponse) {
} 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,
),
)
showSaltPayTapVisaLogoCardDialog()
} else {
navigateTo(AppScreen.Wallet, null)
scope.launch { store.onCardScanned(scanResponse) }
@ -180,6 +221,15 @@ private fun onScanSuccess(scanResponse: ScanResponse) {
}
}
private fun showSaltPayTapVisaLogoCardDialog() {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.saltpay_error_empty_backup_title,
messageId = R.string.saltpay_error_empty_backup_message,
),
)
}
private fun onScanFailure(error: TangemError) {
store.dispatch(HomeAction.ScanInProgress(false))
changeButtonState(ButtonState.ENABLED)

View file

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

View file

@ -3,10 +3,11 @@ 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.message.SaltPayRegistrationError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayRegistrationStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.toSaltPayStep
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.update
import com.tangem.tap.preferencesStorage
/**
[REDACTED_AUTHOR]
@ -14,21 +15,36 @@ import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.toSaltPa
class OnboardingSaltPayHelper {
companion object {
suspend fun isOnboardingCase(scanResponse: ScanResponse): Result<Boolean> {
suspend fun isOnboardingCase(
scanResponse: ScanResponse,
manager: SaltPayActivationManager,
): Result<Boolean> {
return try {
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
val registrationResponseItem = manager.checkRegistration().successOr { return it }
val registrationStep = registrationResponseItem.toSaltPayStep()
manager.checkHasGas().successOr { return it }
var updatedStep = manager.update(SaltPayActivationStep.None, null).successOr { return it }
val isRegistrationOnboardingCase = registrationStep != SaltPayRegistrationStep.Finished
val isBackupOnboardingCase = scanResponse.card.backupStatus?.isActive == false
Result.Success(isRegistrationOnboardingCase || isBackupOnboardingCase)
val cardStorage = preferencesStorage.usedCardsPrefStorage
val activationIsFinished = cardStorage.isActivationFinished(scanResponse.card.cardId)
if (updatedStep == SaltPayActivationStep.Success && activationIsFinished) {
updatedStep = SaltPayActivationStep.Finished
}
val isActivationCase = updatedStep != SaltPayActivationStep.Finished
val isBackupCase = scanResponse.card.backupStatus?.isActive == false
Result.Success(isActivationCase || isBackupCase)
} catch (ex: Exception) {
Result.Failure(ex)
} catch (error: SaltPayRegistrationError){
} catch (error: SaltPayActivationError) {
Result.Failure(error)
}
}
fun testProceedToOnboarding(
scanResponse: ScanResponse,
manager: SaltPayActivationManager,
): Result<Boolean> = Result.Success(true)
fun testProceedToMainScreen(
scanResponse: ScanResponse,
manager: SaltPayActivationManager,
): Result<Boolean> = Result.Success(false)
}
}

View file

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

View file

@ -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.onboardingWalletContainer)
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,7 +195,7 @@ 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(onboardingWalletContainer)
@ -213,13 +204,4 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
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)
}
}

View file

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

View file

@ -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(onboardingWalletContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingWalletContainer, 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)
}
}

View file

@ -139,7 +139,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
val manager = TwinCardsManager(
card = getScanResponse().card,
assetReader = action.reader,
analyticsHandler = store.state.globalState.analyticsHandlers
analyticsHandler = store.state.globalState.analyticsHandler
)
store.dispatch(TwinCardsAction.CardsManager.Set(manager))

View file

@ -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.onboardingWalletContainer)
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,184 +203,175 @@ 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.onboardingWalletContainer.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(onboardingWalletContainer)
@ -390,11 +381,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
}
}
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()
},
)
}
}

View file

@ -5,7 +5,7 @@ import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
object Init : OnboardingWalletAction()
object StartSaltPayCardActivation: OnboardingWalletAction()
object StartSaltPayCardActivation : OnboardingWalletAction()
object GetToCreateWalletStep : OnboardingWalletAction()
object GetToSaltPayStep : OnboardingWalletAction()
object CreateWallet : OnboardingWalletAction()
@ -30,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()

View file

@ -3,13 +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.backupService
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -18,6 +18,7 @@ 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
@ -60,24 +61,26 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
when (action) {
OnboardingWalletAction.Init -> {
// if (walletState.onboardingSaltPayState?.isTest == true) {
// store.dispatch(BackupAction.FinishBackup)
// return
// }
when {
card == null -> {
// StartForUnfinishedBackup -> navigateTo(OnboardingWallet) -> OnboardingWalletAction.Init
// 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 -> {
if (scanResponse.isSaltPay()) {
store.dispatch(OnboardingWalletAction.GetToSaltPayStep)
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)
}
store.dispatch(BackupAction.FinishBackup)
}
else -> {
store.dispatch(OnboardingWalletAction.GetToCreateWalletStep)
@ -87,10 +90,8 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
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)
}
if (onboardingWalletState.isSaltPay && scanResponse != null) {
startCardActivation(scanResponse)
}
}
is OnboardingWalletAction.LoadArtwork -> {
@ -128,7 +129,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
card = result.data.card,
),
)
onboardingManager.activationStarted(updatedResponse.card.cardId)
startCardActivation(updatedResponse)
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
is CompletionResult.Failure -> {
@ -141,13 +142,15 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
OnboardingWalletAction.FinishOnboarding -> {
store.dispatch(GlobalAction.Onboarding.Stop)
//TODO: SaltPay: scanResponse
if (scanResponse == null) {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(HomeAction.ReadCard)
} else {
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
if (onboardingWalletState.isSaltPay) {
finishCardActivation(backupState, scanResponse.card)
}
scope.launch { globalState.tapWalletManager.onCardScanned(updatedScanResponse) }
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
@ -173,16 +176,9 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
newAction?.let { store.dispatch(it) }
}
OnboardingWalletAction.OnBackPressed -> {
when (onboardingWalletState.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)
}
}
}
@ -316,31 +312,24 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
is BackupAction.FinishBackup -> {
(listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds)
.distinct().filterNotNull()
.forEach { cardId ->
preferencesStorage.usedCardsPrefStorage.activationFinished(cardId)
}
if (!onboardingWalletState.isSaltPay) {
finishCardActivation(backupState, card)
}
}
is BackupAction.DiscardBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.DiscardSavedBackup -> {
backupService.primaryCardId?.let {
preferencesStorage.usedCardsPrefStorage.activationFinished(it)
}
backupService.primaryCardId?.let { finishCardsActivationForDiscardedUnfinishedBackup(it) }
backupService.discardSavedBackup()
}
is BackupAction.CheckForUnfinishedBackup -> {
if (backupService.hasIncompletedBackup) {
if (backupService.hasIncompletedBackup && !backupService.primaryCardIsSaltPayVisa()) {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound))
}
}
is BackupAction.ResumeFoundUnfinishedBackup -> {
val isSaltPay = backupService.primaryCardId?.slice(0..3)?.let {
SaltPayWorkaround.isVisaBatchId(it)
} ?: false
store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup(isSaltPay))
store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup)
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
is BackupAction.DismissBackup -> {
@ -350,6 +339,33 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
/**
* Standard Wallet cards start activation at OnboardingWalletAction.CreateWallet
* SaltPay cards start activation at OnboardingWalletAction.StartSaltPayCardActivation
*/
private fun startCardActivation(scanResponse: ScanResponse) {
preferencesStorage.usedCardsPrefStorage.activationStarted(scanResponse.card.cardId)
}
/**
* Standard Wallet cards finish activation at BackupAction.FinishBackup
* SaltPay cards finish activation at OnboardingWalletAction.FinishOnboarding
*/
private fun finishCardActivation(backupState: BackupState, card: Card?) {
(listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds)
.distinct().filterNotNull()
.forEach { cardId ->
preferencesStorage.usedCardsPrefStorage.activationFinished(cardId)
}
}
/**
* Not used for SaltPay cards, because discarding unfinished backup is not possible
*/
private fun finishCardsActivationForDiscardedUnfinishedBackup(cardId: String) {
preferencesStorage.usedCardsPrefStorage.activationFinished(cardId)
}
/**
* SaltPay state maybe not initialized if backup resumed from ResumeFoundUnfinishedBackup action
*/
@ -358,11 +374,38 @@ private fun initSaltPayOnBackupFinishedIfNeeded(
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))
}
}

View file

@ -38,26 +38,28 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
private class ReducerForGlobalAction {
companion object {
fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState = 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()
)
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
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
OnboardingWalletState(
backupState = state.backupState.copy(
maxBackupCards = if (action.isSaltPayBackup) 1 else 2,
canSkipBackup = false,
),
isSaltPay = action.isSaltPayBackup,
)
}
else -> state
}
}
}
@ -74,7 +76,7 @@ private class BackupReducer {
is BackupAction.IntroduceBackup -> BackupState(
backupStep = BackupStep.InitBackup,
maxBackupCards = if (isSaltPay) 1 else 2,
canSkipBackup = !isSaltPay,
canSkipBackup = state.canSkipBackup,
)
BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard)
BackupAction.StartAddingBackupCards -> {

View file

@ -5,7 +5,7 @@ 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.SaltPayRegistrationStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayActivationStep
import org.rekotlin.StateType
/**
@ -41,7 +41,6 @@ data class OnboardingWalletState(
when (backupState.backupStep) {
BackupStep.InitBackup -> 2
BackupStep.ScanOriginCard -> 3
BackupStep.AddBackupCards -> 4
BackupStep.EnterAccessCode -> 4
BackupStep.ReenterAccessCode -> 4
@ -52,13 +51,16 @@ data class OnboardingWalletState(
}
step == OnboardingWalletStep.SaltPay && isSaltPay -> {
when (onboardingSaltPayState!!.step) {
SaltPayRegistrationStep.NoGas -> 1
SaltPayRegistrationStep.NeedPin -> 7
SaltPayRegistrationStep.CardRegistration -> 8
SaltPayRegistrationStep.KycIntro -> 9
SaltPayRegistrationStep.KycStart -> 10
SaltPayRegistrationStep.KycWaiting -> 11
SaltPayRegistrationStep.Finished -> getMaxProgress()
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.ClaimSuccess, SaltPayActivationStep.Success, SaltPayActivationStep.Finished,
-> getMaxProgress()
}
}
step == OnboardingWalletStep.Done -> getMaxProgress()

View file

@ -7,6 +7,7 @@ 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
@ -42,6 +43,8 @@ class GnosisRegistrator(
else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain")
}
private val addressTreasureSafe = "0x8e9260a049d3Aa9ac60D0d4F27017320E0e2396B"
private val atomicNonce = AtomicLong()
suspend fun checkHasGas(): Result<Boolean> {
@ -142,6 +145,21 @@ class GnosisRegistrator(
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 -> {

View file

@ -2,6 +2,13 @@ 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.BlockchainSdkError
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
@ -10,23 +17,24 @@ 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.network.api.paymentology.tryExtractError
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.SaltPayRegistrationError
import com.tangem.tap.persistence.SaltPayRegistrationStorage
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class SaltPayRegistrationManager(
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: SaltPayRegistrationStorage,
) {
val kycUrlProvider = KYCUrlProvider(walletPublicKey, kycProvider)
@ -35,18 +43,14 @@ class SaltPayRegistrationManager(
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(SaltPayRegistrationError.NoGas)
Result.Failure(SaltPayActivationError.NoGas)
}
is com.tangem.blockchain.extensions.Result.Failure -> Result.Failure(SaltPayRegistrationError.NoGas)
is com.tangem.blockchain.extensions.Result.Failure -> Result.Failure(hasGasResult.error as BlockchainSdkError)
}
}
@ -58,20 +62,27 @@ class SaltPayRegistrationManager(
}
suspend fun checkRegistration(): Result<RegistrationResponse.Item> {
val registrationResponse = paymentologyService.checkRegistration(cardId, cardPublicKey).successOr { return it }
val response: RegistrationResponse = paymentologyService.checkRegistration(cardId, cardPublicKey)
.successOr { return it }
.tryExtractError<RegistrationResponse>()
.successOr { return it }
return try {
if (!registrationResponse.success || registrationResponse.results.isEmpty()) {
throw SaltPayRegistrationError.EmptyResponse(registrationResponse.error)
}
Result.Success(registrationResponse.results[0])
} catch (ex: SaltPayRegistrationError) {
if (response.results.isEmpty()) throw SaltPayActivationError.EmptyResponse
val item = response.results[0]
if (item.error != null) throw Exception(response.makeErrorMessage())
Result.Success(item)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
suspend fun requestAttestationChallenge(): Result<AttestationResponse> {
return paymentologyService.requestAttestationChallenge(cardId, cardPublicKey)
.successOr { return it }
.tryExtractError()
}
suspend fun sendTransactions(
@ -94,14 +105,41 @@ class SaltPayRegistrationManager(
cardSignature = attestResponse.cardSignature ?: byteArrayOf(),
pin = pinCode,
)
return paymentologyService.registerWallet(request)
.successOr { return it }
.tryExtractError()
}
val result = paymentologyService.registerWallet(request)
suspend fun getAmountToClaim(): Result<Amount> {
val amount = gnosisRegistrator.getAllowance().successOr {
return Result.Failure(SaltPayActivationError.FailedToGetFundsToClaim)
}
registrationStorage.data = registrationStorage.data.copy(
transactionsSent = result is Result.Success,
)
return if (amount.value == null || amount.value?.isZero() == true) {
Result.Failure(SaltPayActivationError.NoFundsToClaim)
} else {
Result.Success(amount)
}
}
return result
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 {
@ -122,11 +160,10 @@ class SaltPayRegistrationManager(
)
companion object {
fun stub(): SaltPayRegistrationManager = SaltPayRegistrationManager(
fun stub(): SaltPayActivationManager = SaltPayActivationManager(
kycProvider = SaltPayConfig.stub().kycProvider,
paymentologyService = PaymentologyApiService.stub(),
gnosisRegistrator = GnosisRegistrator.stub(),
registrationStorage = SaltPayRegistrationStorage.stub(),
cardId = "",
cardPublicKey = byteArrayOf(),
walletPublicKey = byteArrayOf(),

View file

@ -0,0 +1,46 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import com.tangem.blockchain.common.BlockchainSdkError
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
}
}
}
is BlockchainSdkError -> {
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(throwable.customMessage))
}
else -> {
val message = throwable.localizedMessage ?: "SaltPay unknown error"
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message))
}
}
}
}
}

View file

@ -14,7 +14,7 @@ import com.tangem.wallet.R
*/
class RegistrationErrorDialog {
companion object {
fun create(context: Context, dialog: SaltPayDialog.RegistrationError): Dialog {
fun create(context: Context, dialog: SaltPayDialog.Activation.OnError): Dialog {
val convertedMessage = SaltPayErrorConverter(context).convert(dialog.error) as ConvertedDialogMessage
return AlertDialog.Builder(context).apply {

View file

@ -1,12 +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.SaltPayRegistrationError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
/**
[REDACTED_AUTHOR]
*/
sealed class SaltPayDialog : StateDialog {
object NoFundsForActivation : SaltPayDialog()
data class RegistrationError(val error: SaltPayRegistrationError) : SaltPayDialog()
sealed class Activation : SaltPayDialog() {
object NoGas : SaltPayDialog()
data class OnError(val error: SaltPayActivationError) : SaltPayDialog()
data class TryToInterrupt(val onOk: VoidCallback, val onCancel: VoidCallback) : SaltPayDialog()
}
}

View file

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

View file

@ -20,7 +20,7 @@ sealed class SaltPayModuleError(
}
}
sealed class SaltPayRegistrationError(
sealed class SaltPayActivationError(
subCode: Int,
message: String? = null,
override val data: Any? = null,
@ -28,22 +28,27 @@ sealed class SaltPayRegistrationError(
subCode = ERROR_CODE_REGISTRATION + subCode,
message = message ?: EMPTY_MESSAGE,
) {
object Unknown : SaltPayRegistrationError(4)
object Empty : SaltPayRegistrationError(4)
object Unknown : SaltPayActivationError(4)
object Empty : SaltPayActivationError(4)
// gnosis
object FailedToMakeTxData : SaltPayRegistrationError(1)
object FailedToSendTx : SaltPayRegistrationError(2)
object FailedToMakeTxData : SaltPayActivationError(1)
object FailedToSendTx : SaltPayActivationError(2)
object NeedPin : SaltPayRegistrationError(3)
object NoGas : SaltPayRegistrationError(5) //
class EmptyResponse(message: String?) : SaltPayRegistrationError(6, message)
class CardNotFound(message: String?) : SaltPayRegistrationError(8, message)
class CardDisabled(message: String?) : SaltPayRegistrationError(9, message)
class CardNotPassed(message: String?) : SaltPayRegistrationError(10, message)
object EmptyDynamicAttestResponse : SaltPayRegistrationError(11)
object EmptyBackupCardScanned : SaltPayRegistrationError(12) //
object WeakPin : SaltPayRegistrationError(13) //
object NeedPin : SaltPayActivationError(3)
object NoGas : SaltPayActivationError(5) //
object EmptyResponse : SaltPayActivationError(6)
object CardNotFound : SaltPayActivationError(8)
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 = ""

View file

@ -1,8 +1,10 @@
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 com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayRegistrationManager
import org.rekotlin.Action
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
@ -10,24 +12,28 @@ import org.rekotlin.Action
sealed class OnboardingSaltPayAction : Action {
sealed class Init : OnboardingSaltPayAction() {
data class SetDependencies(
val registrationManager: SaltPayRegistrationManager,
val registrationManager: SaltPayActivationManager,
val saltPayConfig: SaltPayConfig,
) : Init()
// only for tests
object DiscardBackupSteps : Init()
}
data class SetIsBusy(val isBusy: Boolean) : OnboardingSaltPayAction()
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 KYCStart : OnboardingSaltPayAction()
object OnFinishKYC : 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: SaltPayRegistrationStep? = null) : OnboardingSaltPayAction()
data class SetStep(val newStep: SaltPayActivationStep) : OnboardingSaltPayAction()
data class SetAmountToClaim(val amount: Amount?) : OnboardingSaltPayAction()
data class SetTokenBalance(val balanceValue: BigDecimal) : OnboardingSaltPayAction()
}

View file

@ -1,27 +1,31 @@
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.core.TangemSdkError
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.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
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.SaltPayRegistrationManager
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError
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
@ -55,10 +59,27 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
fun getAppState(): AppState = appState()!!
fun getOnboardingWalletState(): OnboardingWalletState = getAppState().onboardingWalletState
fun getState(): OnboardingSaltPayState = getOnboardingWalletState().onboardingSaltPayState!!
val analyticsHandler = getAppState().globalState.analyticsHandler
when (action) {
is OnboardingSaltPayAction.Update -> {
handleInProgress = true
val state = getState()
scope.launch {
val updateStep = state.saltPayManager.update(state.step, state.amountToClaim).successOr {
onException(it.error)
return@launch
}
handleInProgress = false
dispatchOnMain(OnboardingSaltPayAction.SetStep(updateStep))
}
}
is OnboardingSaltPayAction.RegisterCard -> {
handleIsBusy = true
analyticsHandler.handleAnalyticsEvent(Onboarding.ButtonConnect())
handleInProgress = true
val state = getState()
scope.launch {
@ -69,7 +90,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
}
val pinCode = state.pinCode.guard {
onError(SaltPayRegistrationError.NeedPin)
onError(SaltPayActivationError.NeedPin)
return@launch
}
@ -78,11 +99,6 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
return@launch
}
if (!attestationResponse.success) {
onError(SaltPayRegistrationError.CardNotFound(attestationResponse.error))
return@launch
}
val registrationResponse = tangemSdkManager.runTaskAsync(
runnable = saltPayManager.makeRegistrationTask(attestationResponse.challenge!!),
cardId = saltPayManager.cardId,
@ -97,7 +113,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
}
saltPayManager.sendTransactions(registrationResponse.signedTransactions).successOr {
onError(SaltPayRegistrationError.FailedToSendTx)
onError(SaltPayActivationError.FailedToSendTx)
return@launch
}
@ -106,184 +122,314 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
return@launch
}
withMainContext {
handleIsBusy = false
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.KycIntro))
}
handleInProgress = false
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycIntro))
}
}
is OnboardingSaltPayAction.KYCStart -> {
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.KycStart))
is OnboardingSaltPayAction.OpenUtorgKYC -> {
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycStart))
}
is OnboardingSaltPayAction.OnFinishKYC -> {
is OnboardingSaltPayAction.UtorgKYCRedirectSuccess -> {
store.dispatch(OnboardingSaltPayAction.RegisterKYC)
}
is OnboardingSaltPayAction.RegisterKYC -> {
val state = getState()
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycWaiting))
handleInProgress = true
scope.launch {
registerKYCIfNeeded(getState()).successOr {
Timber.d("saltPayManager.registerKYC()")
state.saltPayManager.registerKYC().successOr {
onException(it.error)
return@launch
}
handleInProgress = false
dispatchOnMain(OnboardingSaltPayAction.Update)
}
}
is OnboardingSaltPayAction.TrySetPin -> {
try {
assertPinValid(action.pin, getState().pinLength)
analyticsHandler.handleAnalyticsEvent(Onboarding.PinCodeSet())
store.dispatch(OnboardingSaltPayAction.SetPin(action.pin))
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.CardRegistration))
} catch (error: SaltPayRegistrationError) {
handleError = error
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.CardRegistration))
} catch (error: SaltPayActivationError) {
SaltPayExceptionHandler.handle(error)
}
}
is OnboardingSaltPayAction.Update -> {
handleIsBusy = true
is OnboardingSaltPayAction.Claim -> {
analyticsHandler.handleAnalyticsEvent(Onboarding.ButtonClaim())
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
}
handleInProgress = false
analyticsHandler.handleAnalyticsEvent(Onboarding.ClaimWasSuccessfully())
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimInProgress))
dispatchOnMain(OnboardingSaltPayAction.RefreshClaim)
}
}
is OnboardingSaltPayAction.RefreshClaim -> {
handleClaimRefreshInProgress = true
val state = getState()
scope.launch {
checkGasIfNeeded(state.saltPayManager, state.step).successOr {
onException(it.error)
return@launch
}
when (val amountToClaimResult = state.saltPayManager.getAmountToClaim()) {
is Result.Success -> {
// need to re claim
handleClaimRefreshInProgress = false
dispatchOnMain(OnboardingSaltPayAction.SetAmountToClaim(amountToClaimResult.data))
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Claim))
}
is Result.Failure -> {
when (amountToClaimResult.error) {
is SaltPayActivationError.NoFundsToClaim -> {
val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr {
SaltPayExceptionHandler.handle(it.error)
handleClaimRefreshInProgress = false
return@launch
}
registerKYCIfNeeded(state).successOr {
onException(it.error)
return@launch
}
val newStep = checkRegistration(state).successOr {
onException(it.error)
return@launch
}
withMainContext {
handleIsBusy = false
store.dispatch(OnboardingSaltPayAction.SetStep(newStep))
handleClaimRefreshInProgress = false
if (tokenAmountValue.isPositive()) {
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue))
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess))
}
}
else -> {
handleClaimRefreshInProgress = false
SaltPayExceptionHandler.handle(amountToClaimResult.error)
}
}
}
}
}
}
is OnboardingSaltPayAction.SetStep -> handleAnalytics(analyticsHandler, action.newStep)
else -> {
/* do nothing, only reduce */
}
}
}
private var handleIsBusy: Boolean = false
set(value) {
field = value
store.dispatchOnMain(OnboardingSaltPayAction.SetIsBusy(value))
private fun handleAnalytics(analyticsHandler: GlobalAnalyticsEventHandler, step: SaltPayActivationStep) {
when (step) {
SaltPayActivationStep.KycStart -> analyticsHandler.handleAnalyticsEvent(Onboarding.KYCStarted())
SaltPayActivationStep.KycWaiting -> analyticsHandler.handleAnalyticsEvent(Onboarding.KYCInProgress())
SaltPayActivationStep.KycReject -> analyticsHandler.handleAnalyticsEvent(Onboarding.KYCRejected())
SaltPayActivationStep.Claim -> analyticsHandler.handleAnalyticsEvent(Onboarding.ClaimScreenOpened())
else -> {}
}
}
suspend fun SaltPayActivationManager.update(
currentStep: SaltPayActivationStep,
currentAmountToClaim: Amount?,
): Result<SaltPayActivationStep> {
Timber.d("updateSaltPayStatus")
val registrationResponse = checkRegistration().successOr { return it }
val amountToClaim = getAmountToClaimIfNeeded(this, currentAmountToClaim)
val newStep = try {
determineStep(currentStep, amountToClaim, registrationResponse)
} catch (ex: Exception) {
return Result.Failure(ex)
}
private suspend fun onError(error: SaltPayRegistrationError) {
checkGasIfNeeded(this, newStep).successOr { return it }
Timber.d("update: success: %s", newStep)
return Result.Success(newStep)
}
private fun determineStep(
currentStep: SaltPayActivationStep,
amountToClaim: Amount?,
response: RegistrationResponse.Item,
): SaltPayActivationStep {
fun getStepBaseOnAmountToClaim(amount: Amount?): SaltPayActivationStep = when (amount) {
null -> SaltPayActivationStep.Success
else -> SaltPayActivationStep.Claim
}
return when {
response.passed != true -> throw SaltPayActivationError.CardNotPassed(response.error)
response.disabledByAdmin == true -> throw SaltPayActivationError.CardDisabled(response.error)
// go to claim screen
response.active == true -> getStepBaseOnAmountToClaim(amountToClaim)
// pinSet is false, go toPin screen
response.pinSet == false -> SaltPayActivationStep.NeedPin
response.kycStatus != null -> {
when (currentStep) {
SaltPayActivationStep.KycWaiting -> when (response.kycStatus) {
KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycIntro
KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting
KYCStatus.CORRECTION_REQUESTED, KYCStatus.REJECTED -> SaltPayActivationStep.KycReject
KYCStatus.APPROVED -> getStepBaseOnAmountToClaim(amountToClaim)
else -> SaltPayActivationStep.KycIntro
}
SaltPayActivationStep.KycReject -> when (response.kycStatus) {
KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycStart
KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting
KYCStatus.CORRECTION_REQUESTED -> SaltPayActivationStep.KycStart
KYCStatus.REJECTED -> SaltPayActivationStep.KycStart
KYCStatus.APPROVED -> getStepBaseOnAmountToClaim(amountToClaim)
else -> SaltPayActivationStep.KycIntro
}
else -> when (response.kycStatus) {
KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycIntro
KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting
KYCStatus.CORRECTION_REQUESTED, KYCStatus.REJECTED -> SaltPayActivationStep.KycReject
KYCStatus.APPROVED -> getStepBaseOnAmountToClaim(amountToClaim)
else -> SaltPayActivationStep.KycIntro
}
}
}
// kycDate is set, go to kyc waiting screen
response.kycDate != null -> SaltPayActivationStep.KycWaiting
else -> SaltPayActivationStep.KycIntro
}
}
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 {
handleIsBusy = false
handleError = error
handleInProgress = false
SaltPayExceptionHandler.handle(error)
}
}
private suspend fun onException(error: Throwable) {
withMainContext {
handleIsBusy = false
handleException = error
handleInProgress = false
SaltPayExceptionHandler.handle(error)
}
}
private var handleException: Throwable = Throwable("Init")
set(value) {
field = value
when (value) {
is SaltPayRegistrationError -> handleError = value
else -> {
Timber.e(value)
when (value) {
is TangemSdkError -> {
// store.dispatchErrorNotification(TapError.CustomError(value.customMessage))
}
else -> {
store.dispatchErrorNotification(
TapError.CustomError(value.message ?: "Unknown SaltPay exception"),
)
}
}
}
}
}
private var handleError: SaltPayRegistrationError = SaltPayRegistrationError.Unknown
set(value) {
field = value
when (value) {
SaltPayRegistrationError.NoGas -> {
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.NoGas))
}
else -> {
store.dispatchDialogShow(SaltPayDialog.RegistrationError(value))
}
}
}
private suspend fun checkGasIfNeeded(
saltPayManager: SaltPayRegistrationManager,
step: SaltPayRegistrationStep,
saltPayManager: SaltPayActivationManager,
step: SaltPayActivationStep,
): Result<Unit> {
if (step == SaltPayRegistrationStep.KycStart ||
step == SaltPayRegistrationStep.KycWaiting ||
step == SaltPayRegistrationStep.Finished
) {
Timber.d("checkGasIfNeeded: for step: %s", step)
if (step.ordinal >= SaltPayActivationStep.KycIntro.ordinal) {
Timber.d("checkGasIfNeeded: no need to check for step: %s", step)
return Result.Success(Unit)
}
return saltPayManager.checkHasGas()
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(state: OnboardingSaltPayState): Result<Unit> {
if (state.step != SaltPayRegistrationStep.KycStart) {
return Result.Success(Unit)
}
withMainContext {
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.KycWaiting))
}
return state.saltPayManager.registerKYC()
}
private suspend fun checkRegistration(
state: OnboardingSaltPayState,
): Result<SaltPayRegistrationStep> {
return try {
val registrationResponseItem = state.saltPayManager.checkRegistration().successOr { return it }
val step = registrationResponseItem.toSaltPayStep()
Result.Success(step)
} catch (ex: SaltPayRegistrationError) {
Result.Failure(ex)
}
}
@Throws(SaltPayRegistrationError::class)
fun RegistrationResponse.Item.toSaltPayStep(): SaltPayRegistrationStep {
@Throws(SaltPayActivationError::class)
fun RegistrationResponse.Item.toSaltPayStep(currentStep: SaltPayActivationStep): SaltPayActivationStep {
return when {
passed != true -> throw SaltPayRegistrationError.CardNotPassed(this.error)
disabledByAdmin == true -> throw SaltPayRegistrationError.CardDisabled(this.error)
passed != true -> throw SaltPayActivationError.CardNotPassed(this.error)
disabledByAdmin == true -> throw SaltPayActivationError.CardDisabled(this.error)
// go to success screen
active == true -> SaltPayRegistrationStep.Finished
// go to claim screen
active == true -> SaltPayActivationStep.Claim
// pinSet is false, go toPin screen
pinSet == false -> SaltPayRegistrationStep.NeedPin
pinSet == false -> SaltPayActivationStep.NeedPin
kycStatus != null -> {
when (kycStatus) {
KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycIntro
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 -> SaltPayRegistrationStep.KycWaiting
kycDate != null -> SaltPayActivationStep.KycWaiting
else -> SaltPayRegistrationStep.KycIntro
else -> SaltPayActivationStep.KycIntro
}
}
@Throws(SaltPayRegistrationError::class)
@Throws(SaltPayActivationError::class)
private fun assertPinValid(pin: String, pinLength: Int) {
val array = pin.toCharArray()
if (array.size < pinLength) throw SaltPayRegistrationError.WeakPin
if (array.size < pinLength) throw SaltPayActivationError.WeakPin
val filters: List<Filter<String>> = listOf(
AllSymbolsTheSameFilter(),
)
if (filters.any { it.filter(pin) }) {
throw SaltPayRegistrationError.WeakPin
throw SaltPayActivationError.WeakPin
}
}

View file

@ -1,8 +1,6 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import org.rekotlin.Action
/**
@ -31,20 +29,6 @@ private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingW
val oldState = onboardingWalletState.onboardingSaltPayState ?: return onboardingWalletState
val newState = when (action) {
OnboardingSaltPayAction.Init.DiscardBackupSteps -> {
return onboardingWalletState.copy(
step = OnboardingWalletStep.SaltPay,
backupState = onboardingWalletState.backupState.copy(
backupStep = BackupStep.Finished,
maxBackupCards = 1,
canSkipBackup = false,
),
onboardingSaltPayState = oldState.copy(
isTest = true,
step = SaltPayRegistrationStep.NeedPin,
),
)
}
is OnboardingSaltPayAction.SetAccessCode -> {
oldState.copy(
accessCode = action.accessCode,
@ -62,19 +46,27 @@ private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingW
)
}
}
is OnboardingSaltPayAction.SetIsBusy -> {
is OnboardingSaltPayAction.SetAmountToClaim -> {
oldState.copy(
isBusy = action.isBusy,
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
// is OnboardingSaltPayAction.Init.PrepareAndSetDependencies -> oldState
// OnboardingSaltPayAction.OnCreate -> oldState
// is OnboardingSaltPayAction.OnFinishKYC -> oldState
// is OnboardingSaltPayAction.Register -> oldState
// is OnboardingSaltPayAction.TrySetPin -> oldState
// is OnboardingSaltPayAction.Update -> oldState
// is OnboardingSaltPayAction.TryUpdateStep -> oldState
}
return when (newState) {

View file

@ -1,45 +1,48 @@
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.onboarding.products.wallet.saltPay.SaltPayRegistrationManager
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.persistence.SaltPayRegistrationStorage
import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
data class OnboardingSaltPayState(
@Transient
val saltPayManager: SaltPayRegistrationManager = SaltPayRegistrationManager.stub(),
val saltPayManager: SaltPayActivationManager = SaltPayActivationManager.stub(),
val saltPayConfig: SaltPayConfig = SaltPayConfig.stub(),
val pinCode: String? = null,
val accessCode: String? = null,
val step: SaltPayRegistrationStep = SaltPayRegistrationStep.NeedPin,
val amountToClaim: Amount? = null,
val tokenAmount: Amount = Amount(SaltPayWorkaround.tokenFrom(Blockchain.SaltPay), BigDecimal.ZERO),
val step: SaltPayActivationStep = SaltPayActivationStep.None,
val saltPayCardArtworkUrl: String? = null,
val isBusy: Boolean = false,
val isTest: Boolean = false,
val inProgress: Boolean = false,
val claimInProgress: Boolean = false,
) {
val mainButtonState: WidgetState
get() = if (isBusy) ProgressState.Loading else ProgressState.Done
get() = if (inProgress) ProgressState.Loading else ProgressState.Done
fun readyToClaim(): Boolean = amountToClaim != null
val pinLength: Int = 4
companion object {
fun initDependency(scanResponse: ScanResponse): Pair<SaltPayRegistrationManager, SaltPayConfig> {
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")
@ -52,32 +55,17 @@ data class OnboardingSaltPayState(
scanResponse = scanResponse,
gnosisRegistrator = gnosisRegistrator,
paymentologyService = store.state.domainNetworks.paymentologyService,
registrationStorage = preferencesStorage.saltPayRegistrationStorage,
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: SaltPayRegistrationStorage,
kycProvider: KYCProvider,
): SaltPayRegistrationManager {
): SaltPayActivationManager {
if (!scanResponse.isSaltPay()) {
throw IllegalArgumentException("Can't initialize the OnboardingSaltPayMiddleware if card is not SalPay")
}
@ -85,14 +73,13 @@ data class OnboardingSaltPayState(
throw NullPointerException("SaltPay card must have one wallet at least")
}
return SaltPayRegistrationManager(
return SaltPayActivationManager(
cardId = scanResponse.card.cardId,
cardPublicKey = scanResponse.card.cardPublicKey,
walletPublicKey = saltPaySingleWallet.publicKey,
kycProvider = kycProvider,
paymentologyService = paymentologyService,
gnosisRegistrator = gnosisRegistrator,
registrationStorage = registrationStorage,
)
}
@ -107,12 +94,18 @@ data class OnboardingSaltPayState(
}
}
enum class SaltPayRegistrationStep {
enum class SaltPayActivationStep {
None,
NoGas,
NeedPin,
CardRegistration,
KycIntro,
KycStart,
KycWaiting,
KycReject,
Claim,
ClaimInProgress,
ClaimSuccess,
Success,
Finished;
}

View file

@ -0,0 +1,388 @@
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.ClaimSuccess -> handleClaim(state)
SaltPayActivationStep.Success -> handleSuccess(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 = getText(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 = getText(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 = getText(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 = getText(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 = getText(R.string.onboarding_navbar_kyc_progress)
showOnlyView(root)
imvInProgress.setDrawable(R.drawable.ic_in_progress)
tvHeader.text = getText(R.string.onboarding_title_kyc_waiting)
tvBody.text = getText(R.string.onboarding_subtitle_kyc_waiting)
btnOpenSupportChat.hide()
btnOpenSupportChat.setOnClickListener {
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
}
btnKycAction.text = getText(R.string.onboarding_button_kyc_waiting)
btnKycAction.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.Update)
}
progressButton = SaltPayProgressButton(root)
}
private fun handleKycReject() = with(walletFragment.bindingSaltPay.kycInProgress) {
toolbar.title = getText(R.string.onboarding_navbar_kyc_progress)
showOnlyView(root)
imvInProgress.setDrawable(R.drawable.ic_reject)
tvHeader.text = getText(R.string.onboarding_title_kyc_retry)
tvBody.text = getText(R.string.onboarding_subtitle_kyc_retry)
btnOpenSupportChat.hide()
btnKycAction.text = getText(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 = getText(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 -> {
claimValueString(state)?.let {
tvHeader.text = getText(R.string.onboarding_title_claim, it)
}
tvBody.text = getText(R.string.onboarding_subtitle_claim)
btnMain.text = getText(R.string.onboarding_button_claim)
btnMain.isEnabled
btnMain.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.Claim)
}
progressButton = SaltPayProgressButton(actionContainer.root)
progressButton?.isEnabled = true
}
SaltPayActivationStep.ClaimInProgress -> {
tvHeader.text = getText(R.string.onboarding_title_claim_progress)
tvBody.text = getText(R.string.onboarding_subtitle_claim_progress)
btnMain.text = getText(R.string.onboarding_button_claim)
progressButton = SaltPayProgressButton(actionContainer.root)
progressButton?.isEnabled = false
}
SaltPayActivationStep.ClaimSuccess -> {
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)
progressButton = SaltPayProgressButton(actionContainer.root)
progressButton?.isEnabled = true
}
else -> {}
}
}
private fun handleSuccess(state: OnboardingSaltPayState) = with(walletFragment.binding) {
walletFragment.bindingSaltPay.onboardingSaltpayContainer.hide()
onboardingWalletContainer.show()
walletFragment.showSuccess()
tvBody.text = getText(R.string.onboarding_subtitle_success_claim)
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet)
}
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 getText(@StringRes resId: Int): String {
return walletFragment.getString(resId)
}
private fun getText(@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
}
}
}
}

View file

@ -0,0 +1,306 @@
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 showBackupIntro(state: BackupState) {
currentStep = STEP_BACKUP_INTRO
ORIGIN.show()
FIRST_BACKUP.show()
SECOND_BACKUP.show()
cardsWidget.toWelcome()
}
override fun setupCreateWalletState() {
currentStep = STEP_CREATE_WALLET
cardsWidget.toFolded(false)
ORIGIN.alpha(1f)
FIRST_BACKUP.alpha(1f)
SECOND_BACKUP.alpha(1f)
}
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 = 300) { 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)
},
)
}
private var firstBackupCardAnimated = false
private var secondBackupCardAnimated = false
override fun showWriteBackupCard(state: BackupState, backupCard: Int) {
when (viewState) {
State.TwoCards -> {
if (firstBackupCardAnimated) return
firstBackupCardAnimated = true
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 -> {
if (firstBackupCardAnimated) return
firstBackupCardAnimated = true
FIRST_BACKUP.alpha(1f)
cardsWidget.leapfrogWidget.leap {
SECOND_BACKUP.alpha(0.4f)
ORIGIN.alpha(0.2f)
}
}
2 -> {
if (secondBackupCardAnimated) return
secondBackupCardAnimated = true
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)
}
}
}

View file

@ -5,52 +5,34 @@ 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 android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.core.view.MenuProvider
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import by.kirich1409.viewbindingdelegate.viewBinding
import coil.load
import coil.size.Scale
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.button.MaterialButton
import com.google.android.material.tabs.TabLayoutMediator
import com.tangem.common.CardIdFormatter
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.common.extensions.guard
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.compose.PinCodeWidget
import com.tangem.tap.common.extensions.beginDelayedTransition
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.hide
import com.tangem.tap.common.extensions.inflate
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.stop
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.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.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.SaltPayRegistrationStep
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
@ -59,23 +41,21 @@ 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 val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
private val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind)
private val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer }
internal val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind)
internal val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer }
private val saltPayView: SaltPayView = SaltPayView(this)
private val onboardingSaltPayView: OnboardingSaltPayView = OnboardingSaltPayView(this)
private lateinit var cardsWidget: WalletCardsWidget
private var accessCodeDialog: AccessCodeDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
setHasOptionsMenu(true)
private lateinit var animator: BackupAnimator
override fun configureTransitions() {
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
@ -84,16 +64,8 @@ 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(
@ -113,6 +85,39 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
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() {
super.onStart()
store.subscribe(this) { state ->
@ -129,14 +134,16 @@ 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()
pbBinding.pbState.max = state.getMaxProgress()
pbBinding.pbState.progress = state.getProgressStep()
if (state.isSaltPay) {
saltPayView.newState(state)
onboardingSaltPayView.newState(state)
} else {
loadImageIntoImageView(state.cardArtworkUrl, binding.imvFrontCard)
loadImageIntoImageView(state.cardArtworkUrl, binding.imvFirstBackupCard)
@ -145,7 +152,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
}
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)
@ -153,7 +160,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
}
private fun handleOnboardingStep(state: OnboardingWalletState) {
internal fun handleOnboardingStep(state: OnboardingWalletState) {
when (state.step) {
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
OnboardingWalletStep.Backup -> setBackupState(state.backupState, state.isSaltPay)
@ -162,16 +169,16 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
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, isSaltPay: Boolean) {
@ -192,52 +199,36 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
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)
btnMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
btnWalletMainAction.text = getText(R.string.onboarding_button_backup_now)
btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
btnAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
btnAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) }
btnAlternativeAction.show(state.canSkipBackup && !isSaltPay)
btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) }
btnWalletAlternativeAction.show(state.canSkipBackup && !isSaltPay)
}
startPostponedEnterTransition()
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) }
}
}
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()
animator.showScanOriginCard()
}
private fun showAddBackupCards(state: BackupState, isSaltPay: Boolean) = with(binding) {
@ -260,36 +251,38 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0
if (isSaltPay) {
saltPayView.showAddBackupCards(state)
return@with
}
when (state.backupCardsNumber) {
0 -> {
cardsWidget.toFan {
cardsWidget.getFirstBackupCardView().animate().alpha(0.6f).duration = 200
cardsWidget.getSecondBackupCardView().animate().alpha(0.2f).duration = 200
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)
}
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).duration = 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).duration = 400
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() {
@ -323,89 +316,67 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
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))
if (isSaltPay) {
tvHeader.text = getText(R.string.onboarding_saltpay_title_prepare_origin)
tvBody.text = getString(R.string.onboarding_twins_interrupt_warning)
layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_saltpay_button_backup_origin)
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.btnMainAction.text = getText(R.string.onboarding_button_backup_origin)
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_backup_origin)
}
layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
layoutButtonsCommon.btnMainAction.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, 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
}
}
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
if (isSaltPay) {
tvHeader.text = getString(R.string.onboarding_saltpay_title_backup_card)
tvBody.text = getString(R.string.onboarding_twins_interrupt_warning)
layoutButtonsCommon.btnMainAction.text = getString(R.string.onboarding_saltpay_title_backup_card)
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.btnMainAction.text = getString(
layoutButtonsCommon.btnWalletMainAction.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)
@ -415,24 +386,30 @@ 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)?.duration = 400
internal fun showConfetti(show: Boolean) = with(binding.vConfetti) {
lavConfetti.show(show)
if (show) {
lavConfetti.playAnimation()
} else {
lavConfetti.cancelAnimation()
}
}
@ -440,203 +417,9 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
store.dispatch(OnboardingWalletAction.OnBackPressed)
}
private class SaltPayView(
private val walletFragment: OnboardingWalletFragment,
) {
private val toolbar: MaterialToolbar by lazy { walletFragment.binding.toolbar }
private var progressButton: SaltPayProgressButton? = null
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)
//TODO: at now we can hide the image only by changing alpha channel to 0, because
// the OnboardingWalletFragment and WalletCardsWidget manipulate it visibility through changing
// View.VISIBILITY states.
imvSecondBackupCard.alpha = 0f
}
fun showAddBackupCards(state: BackupState) = with(walletFragment.binding) {
walletFragment.cardsWidget.getSecondBackupCardView().alpha = 0f
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)
walletFragment.cardsWidget.getFirstBackupCardView().alpha = 0.6f
walletFragment.cardsWidget.toFan()
}
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)
walletFragment.cardsWidget.toFan(false)
walletFragment.cardsWidget.getFirstBackupCardView().animate().alpha(1f).duration = 400
}
}
}
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) {
SaltPayRegistrationStep.NoGas -> handleNoGas()
SaltPayRegistrationStep.NeedPin -> handleNeedPin()
SaltPayRegistrationStep.CardRegistration -> handleCardRegistration()
SaltPayRegistrationStep.KycIntro -> handleKycIntro()
SaltPayRegistrationStep.KycStart -> handleKycStart(state)
SaltPayRegistrationStep.KycWaiting -> handleKycWaiting()
SaltPayRegistrationStep.Finished -> handleFinished()
}
progressButton?.changeState(state.mainButtonState)
}
private fun handleNoGas() {
// normally this shouldn't happen
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatchDialogShow(SaltPayDialog.NoFundsForActivation)
}
private fun handleNeedPin() = with(walletFragment.bindingSaltPay) {
toolbar.title = walletFragment.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 = walletFragment.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 = walletFragment.getString(R.string.onboarding_navbar_kyc_start)
showOnlyView(verifyIdentity.root) {
progressButton = SaltPayProgressButton(verifyIdentity.root)
verifyIdentity.btnVerify.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.KYCStart)
}
}
}
private fun handleKycStart(state: OnboardingSaltPayState) = with(walletFragment.bindingSaltPay) {
toolbar.title = walletFragment.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.OnFinishKYC)
},
)
webvVerifyIdentity.webChromeClient = UtorgWebChromeClient(walletFragment.requireActivity())
webvVerifyIdentity.loadUrl(state.saltPayManager.kycUrlProvider.requestUrl)
}
}
private fun handleKycWaiting() = with(walletFragment.bindingSaltPay) {
toolbar.title = walletFragment.getString(R.string.onboarding_navbar_kyc_waiting)
showOnlyView(verifyIdentityInProgress.root) {
progressButton = SaltPayProgressButton(verifyIdentityInProgress.root)
verifyIdentityInProgress.btnOpenSupportChat.setOnClickListener {
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
}
verifyIdentityInProgress.btnRefresh.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.Update)
}
}
}
private fun handleFinished() {
walletFragment.binding.onboardingRoot.beginDelayedTransition()
walletFragment.bindingSaltPay.onboardingSaltpayContainer.hide()
walletFragment.binding.onboardingWalletContainer.show()
walletFragment.showSuccess()
}
private fun showOnlyView(view: View, onShowListener: (() -> Unit)? = null) {
// onboardingSaltpayContainer.beginDelayedTransition()
walletFragment.bindingSaltPay.onboardingSaltpayContainer.children
.filter { it.id != view.id }
.forEach { it.hide() }
view.show { onShowListener?.invoke() }
}
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
}
}
}
private fun getDeviceScaleFactor(): Float {
val typedValue = TypedValue()
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
return typedValue.float
}
}
}

View file

@ -16,87 +16,100 @@ class WalletCardsWidget(
val getTopOfAnchorViewForActivateState: () -> Float,
) {
val animDuration: Long = 400
private val animDuration: Long = 400
var currentState: WidgetState? = null
private set
fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) {
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) animDuration 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
}
@ -181,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,
@ -190,24 +203,40 @@ 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 }

View file

@ -17,13 +17,12 @@ import com.tangem.common.services.Result
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.AnalyticsEventAnOld
import com.tangem.tap.common.analytics.AnalyticsParamAnOld
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.logSendTransactionError
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.AppDialog
@ -222,8 +221,8 @@ private fun sendTransaction(
when (sendResult) {
is SimpleResult.Success -> {
store.state.globalState.analyticsHandlers?.triggerEvent(
event = AnalyticsEvent.TRANSACTION_IS_SENT,
store.state.globalState.analyticsHandler?.handleAnalyticsEvent(
event = AnalyticsEventAnOld.TRANSACTION_IS_SENT,
card = card,
blockchain = walletManager.wallet.blockchain.currency,
)
@ -251,10 +250,10 @@ private fun sendTransaction(
feeAmount = feeAmount,
destinationAddress = destinationAddress,
)
store.state.globalState.analyticsHandlers?.logSendTransactionError(
store.state.globalState.analyticsHandler?.handleBlockchainSdkErrorEvent(
error = sendResult.error,
action = Analytics.ActionToLog.SendTransaction,
parameters = mapOf(AnalyticsParam.BLOCKCHAIN to walletManager.wallet.blockchain.currency),
action = AnalyticsAnOld.ActionToLog.SendTransaction,
params = mapOf(AnalyticsParamAnOld.BLOCKCHAIN to walletManager.wallet.blockchain.currency),
card = card,
)

View file

@ -121,7 +121,7 @@ private fun handle(action: Action) {
scope.launch {
shopService.waitForCheckout(
productType = shopState.selectedProduct,
analyticsHandler = store.state.globalState.analyticsHandlers
analyticsHandler = store.state.globalState.analyticsHandler
)
}
}

View file

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

View file

@ -11,7 +11,7 @@ import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
@ -186,9 +186,9 @@ class WalletMiddleware {
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandlers?.logCardSdkError(
store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent(
error,
Analytics.ActionToLog.CreateWallet,
AnalyticsAnOld.ActionToLog.CreateWallet,
card = store.state.detailsState.scanResponse?.card
)
}

View file

@ -8,7 +8,7 @@ import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsEventAnOld
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.isGreaterThan
import com.tangem.tap.common.redux.global.GlobalState
@ -25,10 +25,10 @@ import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import java.math.BigDecimal
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class WarningsMiddleware {
fun handle(action: WalletAction.Warnings, globalState: GlobalState?) {
@ -81,7 +81,7 @@ class WarningsMiddleware {
preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds()
}
if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) {
store.state.globalState.analyticsHandlers?.triggerEvent(AnalyticsEvent.APP_RATING_DISPLAYED)
store.state.globalState.analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_DISPLAYED)
addWarningMessage(WarningMessagesManager.appRatingWarning(), true)
}
}

View file

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

View file

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

View file

@ -139,7 +139,7 @@ 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)
@ -219,6 +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)
}
}

View file

@ -7,7 +7,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.play.core.review.ReviewManagerFactory
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsEventAnOld
import com.tangem.tap.common.extensions.getActivity
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getString
@ -107,14 +107,14 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
binding.groupControlsTemporary.hide()
binding.groupControlsRating.show()
binding.btnClose.show()
val analyticsHandler = store.state.globalState.analyticsHandlers
val analyticsHandler = store.state.globalState.analyticsHandler
binding.btnClose.setOnClickListener {
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS)
analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_DISMISS)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(WalletAction.Warnings.AppRating.RemindLater)
}
binding.btnCanBeBetter.setOnClickListener {
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE)
analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_NEGATIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail()))
@ -122,7 +122,7 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
binding.btnReallyCool.setOnClickListener {
val activity = binding.root.context.getActivity() ?: return@setOnClickListener
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_POSITIVE)
analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_POSITIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
val reviewManager = ReviewManagerFactory.create(activity)
val task = reviewManager.requestReviewFlow()

View file

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

View file

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