Updated on 2026-08-14
This commit is contained in:
parent
d0c4d92a8c
commit
5fea60f2a2
83 changed files with 2801 additions and 775 deletions
|
|
@ -2,7 +2,6 @@ package com.tangem.tap.features.onboarding
|
|||
|
||||
import com.tangem.domain.common.ProductType
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.domain.extensions.hasWallets
|
||||
import com.tangem.tap.preferencesStorage
|
||||
|
|
@ -16,19 +15,14 @@ class OnboardingHelper {
|
|||
fun isOnboardingCase(response: ScanResponse): Boolean {
|
||||
val cardInfoStorage = preferencesStorage.usedCardsPrefStorage
|
||||
return when {
|
||||
response.card.isSaltPay -> {
|
||||
response.card.backupStatus?.isActive != true
|
||||
}
|
||||
response.productType == ProductType.Twins -> {
|
||||
response.isTangemTwins() -> {
|
||||
if (!response.twinsIsTwinned()) {
|
||||
true
|
||||
} else {
|
||||
cardInfoStorage.activationIsStarted(response.card.cardId)
|
||||
}
|
||||
}
|
||||
response.card.hasWallets() -> {
|
||||
cardInfoStorage.activationIsStarted(response.card.cardId)
|
||||
}
|
||||
response.card.hasWallets() -> cardInfoStorage.activationIsStarted(response.card.cardId)
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
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
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class OnboardingSaltPayHelper {
|
||||
companion object {
|
||||
|
||||
suspend fun isOnboardingCase(scanResponse: ScanResponse): 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 }
|
||||
|
||||
val isRegistrationOnboardingCase = registrationStep != SaltPayRegistrationStep.Finished
|
||||
val isBackupOnboardingCase = scanResponse.card.backupStatus?.isActive == false
|
||||
Result.Success(isRegistrationOnboardingCase || isBackupOnboardingCase)
|
||||
} catch (ex: Exception) {
|
||||
Result.Failure(ex)
|
||||
} catch (error: SaltPayRegistrationError){
|
||||
Result.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
startPostponedEnterTransition()
|
||||
|
||||
binding.toolbar.setTitle(R.string.onboarding_title)
|
||||
btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
|
||||
btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingWalletContainer)
|
||||
|
||||
store.dispatch(OnboardingNoteAction.LoadCardArtwork)
|
||||
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
|
||||
|
|
@ -207,10 +207,10 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
|
|||
with(binding.onboardingTopContainer) {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(requireContext(), layoutId)
|
||||
constraintSet.applyTo(onboardingMainContainer)
|
||||
constraintSet.applyTo(onboardingWalletContainer)
|
||||
val transition = InternalNoteLayoutTransition()
|
||||
transition.interpolator = OvershootInterpolator()
|
||||
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
|
||||
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,10 +116,10 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
|
|||
with(binding.onboardingTopContainer) {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(requireContext(), layoutId)
|
||||
constraintSet.applyTo(onboardingMainContainer)
|
||||
constraintSet.applyTo(onboardingWalletContainer)
|
||||
val transition = InternalNoteLayoutTransition()
|
||||
transition.interpolator = OvershootInterpolator()
|
||||
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
|
||||
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
285f * deviceScaleFactorForWelcomeState
|
||||
}
|
||||
btnRefreshBalanceWidget =
|
||||
RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
|
||||
RefreshBalanceWidget(binding.onboardingTopContainer.onboardingWalletContainer)
|
||||
|
||||
binding.toolbar.title = getText(R.string.twins_recreate_toolbar)
|
||||
|
||||
|
|
@ -212,7 +212,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
chbUnderstand.hide()
|
||||
pbBinding.pbState.show()
|
||||
|
||||
binding.onboardingTopContainer.onboardingMainContainer.beginDelayedTransition()
|
||||
binding.onboardingTopContainer.onboardingWalletContainer.beginDelayedTransition()
|
||||
|
||||
when (previousStep) {
|
||||
TwinCardsStep.None -> {
|
||||
|
|
@ -383,10 +383,10 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
|||
with(binding.onboardingTopContainer) {
|
||||
val constraintSet = ConstraintSet()
|
||||
constraintSet.clone(requireContext(), layoutId)
|
||||
constraintSet.applyTo(onboardingMainContainer)
|
||||
constraintSet.applyTo(onboardingWalletContainer)
|
||||
val transition = InternalNoteLayoutTransition()
|
||||
transition.interpolator = OvershootInterpolator()
|
||||
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
|
||||
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ import org.rekotlin.Action
|
|||
|
||||
sealed class OnboardingWalletAction : Action {
|
||||
object Init : OnboardingWalletAction()
|
||||
object StartSaltPayCardActivation: OnboardingWalletAction()
|
||||
object GetToCreateWalletStep : OnboardingWalletAction()
|
||||
object GetToSaltPayStep : OnboardingWalletAction()
|
||||
object CreateWallet : OnboardingWalletAction()
|
||||
object Done : OnboardingWalletAction()
|
||||
object FinishOnboarding : OnboardingWalletAction()
|
||||
|
||||
data class ProceedBackup(val isSaltPay: Boolean = false) : OnboardingWalletAction()
|
||||
object ResumeBackup : OnboardingWalletAction()
|
||||
|
||||
object LoadArtwork : OnboardingWalletAction()
|
||||
class SetArtworkUrl(val artworkUrl: String?) : OnboardingWalletAction()
|
||||
|
|
@ -21,7 +23,7 @@ sealed class OnboardingWalletAction : Action {
|
|||
sealed class BackupAction : Action {
|
||||
|
||||
object DetermineBackupStep : BackupAction()
|
||||
data class IntroduceBackup(val isSaltPay: Boolean = false) : BackupAction()
|
||||
object IntroduceBackup : BackupAction()
|
||||
object StartBackup : BackupAction()
|
||||
object DismissBackup : BackupAction()
|
||||
|
||||
|
|
@ -53,15 +55,13 @@ sealed class BackupAction : Action {
|
|||
data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction()
|
||||
object OnAccessCodeDialogClosed : BackupAction()
|
||||
|
||||
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
|
||||
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
|
||||
|
||||
object PrepareToWritePrimaryCard : BackupAction()
|
||||
object WritePrimaryCard : BackupAction()
|
||||
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
|
||||
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
|
||||
|
||||
object FinishBackup : BackupAction()
|
||||
object DiscardBackup : BackupAction()
|
||||
object DiscardSavedBackup : BackupAction()
|
||||
object ResumeBackup : BackupAction()
|
||||
|
||||
object ResumeFoundUnfinishedBackup : BackupAction()
|
||||
}
|
||||
|
|
@ -4,9 +4,9 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.common.CardFilter
|
||||
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.TapWorkarounds
|
||||
import com.tangem.domain.common.TapWorkarounds.isSaltPay
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.tap.backupService
|
||||
|
|
@ -19,6 +19,8 @@ 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.redux.OnboardingSaltPayAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
import com.tangem.tap.features.wallet.redux.Artwork
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.preferencesStorage
|
||||
|
|
@ -28,6 +30,7 @@ import com.tangem.tap.tangemSdk
|
|||
import com.tangem.tap.tangemSdkManager
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
class OnboardingWalletMiddleware {
|
||||
|
|
@ -39,35 +42,57 @@ class OnboardingWalletMiddleware {
|
|||
private val onboardingWalletMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
handleWalletAction(action)
|
||||
handleWalletAction(action, state, dispatch)
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleWalletAction(action: Action) {
|
||||
private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: DispatchFunction) {
|
||||
if (action !is OnboardingWalletAction) return
|
||||
|
||||
val globalState = store.state.globalState
|
||||
val onboardingManager = globalState.onboardingState.onboardingManager
|
||||
|
||||
val scanResponse = onboardingManager?.scanResponse
|
||||
val card = scanResponse?.card
|
||||
|
||||
val walletState = store.state.onboardingWalletState
|
||||
val onboardingWalletState = store.state.onboardingWalletState
|
||||
|
||||
when (action) {
|
||||
OnboardingWalletAction.Init -> {
|
||||
val action = when {
|
||||
// if (walletState.onboardingSaltPayState?.isTest == true) {
|
||||
// store.dispatch(BackupAction.FinishBackup)
|
||||
// return
|
||||
// }
|
||||
|
||||
when {
|
||||
card == null -> {
|
||||
OnboardingWalletAction.ProceedBackup()
|
||||
// StartForUnfinishedBackup -> navigateTo(OnboardingWallet) -> OnboardingWalletAction.Init
|
||||
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)
|
||||
}
|
||||
store.dispatch(BackupAction.FinishBackup)
|
||||
}
|
||||
else -> {
|
||||
store.dispatch(OnboardingWalletAction.GetToCreateWalletStep)
|
||||
}
|
||||
}
|
||||
}
|
||||
is OnboardingWalletAction.StartSaltPayCardActivation -> {
|
||||
// we need to do this because the standard activation happens in the OnboardingWalletAction.CreateWallet
|
||||
// step which is omitted for SaltPay cards
|
||||
if (onboardingWalletState.isSaltPay) {
|
||||
ifNotNull(onboardingManager, scanResponse?.card?.cardId) { manager, cardId ->
|
||||
manager.activationStarted(cardId)
|
||||
}
|
||||
card.hasWallets() && card.backupStatus == Card.BackupStatus.NoBackup ->
|
||||
OnboardingWalletAction.ProceedBackup(card.isSaltPay)
|
||||
card.hasWallets() && card.backupStatus?.isActive == true ->
|
||||
BackupAction.FinishBackup
|
||||
else -> OnboardingWalletAction.GetToCreateWalletStep
|
||||
}
|
||||
store.dispatch(action)
|
||||
}
|
||||
is OnboardingWalletAction.LoadArtwork -> {
|
||||
scope.launch {
|
||||
|
|
@ -88,25 +113,24 @@ private fun handleWalletAction(action: Action) {
|
|||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
//here we must use updated scanResponse after createWallet & derivation
|
||||
val updatedResponse =
|
||||
globalState.onboardingState.onboardingManager.scanResponse.copy(
|
||||
card = result.data.card,
|
||||
derivedKeys = result.data.derivedKeys,
|
||||
primaryCard = result.data.primaryCard
|
||||
)
|
||||
val updatedResponse = globalState.onboardingState.onboardingManager.scanResponse.copy(
|
||||
card = result.data.card,
|
||||
derivedKeys = result.data.derivedKeys,
|
||||
primaryCard = result.data.primaryCard,
|
||||
)
|
||||
onboardingManager.scanResponse = updatedResponse
|
||||
val blockchainNetworks = listOf(
|
||||
BlockchainNetwork(Blockchain.Bitcoin, result.data.card),
|
||||
BlockchainNetwork(Blockchain.Ethereum, result.data.card)
|
||||
BlockchainNetwork(Blockchain.Ethereum, result.data.card),
|
||||
)
|
||||
store.dispatchOnMain(
|
||||
store.dispatch(
|
||||
WalletAction.MultiWallet.SaveCurrencies(
|
||||
blockchainNetworks = blockchainNetworks,
|
||||
card = result.data.card,
|
||||
),
|
||||
)
|
||||
onboardingManager.activationStarted(updatedResponse.card.cardId)
|
||||
store.dispatch(OnboardingWalletAction.ProceedBackup())
|
||||
store.dispatch(OnboardingWalletAction.ResumeBackup)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
// do nothing
|
||||
|
|
@ -118,6 +142,7 @@ private fun handleWalletAction(action: Action) {
|
|||
OnboardingWalletAction.FinishOnboarding -> {
|
||||
store.dispatch(GlobalAction.Onboarding.Stop)
|
||||
|
||||
//TODO: SaltPay: scanResponse
|
||||
if (scanResponse == null) {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
store.dispatch(HomeAction.ReadCard)
|
||||
|
|
@ -128,15 +153,15 @@ private fun handleWalletAction(action: Action) {
|
|||
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
|
||||
}
|
||||
}
|
||||
is OnboardingWalletAction.ProceedBackup -> {
|
||||
is OnboardingWalletAction.ResumeBackup -> {
|
||||
val newAction = when (val backupState = backupService.currentState) {
|
||||
BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
|
||||
is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index)
|
||||
else -> {
|
||||
if (walletState.backupState.backupStep == BackupStep.InitBackup ||
|
||||
walletState.backupState.backupStep == BackupStep.Finished
|
||||
if (onboardingWalletState.backupState.backupStep == BackupStep.InitBackup ||
|
||||
onboardingWalletState.backupState.backupStep == BackupStep.Finished
|
||||
) {
|
||||
BackupAction.IntroduceBackup(action.isSaltPay)
|
||||
BackupAction.IntroduceBackup
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -145,7 +170,7 @@ private fun handleWalletAction(action: Action) {
|
|||
newAction?.let { store.dispatch(it) }
|
||||
}
|
||||
OnboardingWalletAction.OnBackPressed -> {
|
||||
when (walletState.backupState.backupStep) {
|
||||
when (onboardingWalletState.backupState.backupStep) {
|
||||
BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo())
|
||||
BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,
|
||||
BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard,
|
||||
|
|
@ -167,7 +192,7 @@ private fun updateScanResponseAfterBackup(
|
|||
val cardsCount = backupState.backupCardsNumber
|
||||
scanResponse.card.copy(
|
||||
backupStatus = Card.BackupStatus.Active(cardCount = cardsCount),
|
||||
isAccessCodeSet = true
|
||||
isAccessCodeSet = true,
|
||||
)
|
||||
} else {
|
||||
scanResponse.card
|
||||
|
|
@ -188,13 +213,11 @@ class BackupMiddleware {
|
|||
|
||||
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
|
||||
if (DemoHelper.tryHandle(appState, action)) return
|
||||
val globalState = appState()?.globalState ?: return
|
||||
val onboardingWalletState = appState()?.onboardingWalletState ?: return
|
||||
|
||||
val backupState = store.state.onboardingWalletState.backupState
|
||||
|
||||
val globalState = store.state.globalState
|
||||
val onboardingManager = globalState.onboardingState.onboardingManager
|
||||
|
||||
val scanResponse = onboardingManager?.scanResponse
|
||||
val backupState = onboardingWalletState.backupState
|
||||
val scanResponse = globalState.onboardingState.onboardingManager?.scanResponse
|
||||
val card = scanResponse?.card
|
||||
|
||||
when (action) {
|
||||
|
|
@ -221,8 +244,10 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
}
|
||||
is BackupAction.AddBackupCard -> {
|
||||
backupService.skipCompatibilityChecks = true
|
||||
tangemSdk.config.filter.cardIdFilter =
|
||||
CardFilter.Companion.ItemFilter.Allow(TapWorkarounds.saltPayTangemCardIds.toSet())
|
||||
tangemSdk.config.filter.cardIdFilter = CardFilter.Companion.CardIdFilter.Allow(
|
||||
items = SaltPayWorkaround.tangemWalletCardIds.toSet(),
|
||||
ranges = SaltPayWorkaround.tangemWalletCardIdRanges,
|
||||
)
|
||||
|
||||
backupService.addBackupCard { result ->
|
||||
backupService.skipCompatibilityChecks = false
|
||||
|
|
@ -237,20 +262,15 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
}
|
||||
}
|
||||
is BackupAction.FinishAddingBackupCards -> {
|
||||
if (backupService.addedBackupCardsCount == 1) {
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards))
|
||||
}
|
||||
if (backupService.addedBackupCardsCount == 2) {
|
||||
if (backupService.addedBackupCardsCount == backupState.maxBackupCards) {
|
||||
store.dispatchOnMain(BackupAction.ShowAccessCodeInfoScreen)
|
||||
} else {
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards))
|
||||
}
|
||||
}
|
||||
is BackupAction.CheckAccessCode -> {
|
||||
if (action.accessCode.length < 4) {
|
||||
store.dispatch(
|
||||
BackupAction.SetAccessCodeError(
|
||||
AccessCodeError.CodeTooShort
|
||||
)
|
||||
)
|
||||
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodeTooShort))
|
||||
} else {
|
||||
store.dispatch(BackupAction.SetAccessCodeError(null))
|
||||
store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode))
|
||||
|
|
@ -262,20 +282,14 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
backupService.setAccessCode(action.accessCodeConfirmation)
|
||||
store.dispatch(BackupAction.PrepareToWritePrimaryCard)
|
||||
} else {
|
||||
store.dispatch(
|
||||
BackupAction.SetAccessCodeError(
|
||||
AccessCodeError.CodesDoNotMatch
|
||||
)
|
||||
)
|
||||
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodesDoNotMatch))
|
||||
}
|
||||
}
|
||||
is BackupAction.WritePrimaryCard -> {
|
||||
backupService.proceedBackup { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
store.dispatchOnMain(
|
||||
BackupAction.PrepareToWriteBackupCard(1)
|
||||
)
|
||||
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(1))
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
}
|
||||
|
|
@ -287,15 +301,13 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
if (backupService.currentState == BackupService.State.Finished) {
|
||||
initSaltPayOnBackupFinishedIfNeeded(scanResponse, onboardingWalletState)
|
||||
store.dispatchOnMain(BackupAction.FinishBackup)
|
||||
} else {
|
||||
store.dispatchOnMain(
|
||||
BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1)
|
||||
)
|
||||
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1))
|
||||
}
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -321,13 +333,31 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound))
|
||||
}
|
||||
}
|
||||
is BackupAction.ResumeBackup -> {
|
||||
store.dispatch(GlobalAction.Onboarding.Start(null, true))
|
||||
store.dispatch(OnboardingWalletAction.ProceedBackup())
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
|
||||
is BackupAction.ResumeFoundUnfinishedBackup -> {
|
||||
// val isSaltPay = backupService.primaryBatchId?.let { SaltPayWorkaround.isVisaBatchId(it) } ?: false
|
||||
// store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup(isSaltPay))
|
||||
// store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
|
||||
}
|
||||
is BackupAction.DismissBackup -> {
|
||||
if (onboardingWalletState.isSaltPay) throw UnsupportedOperationException()
|
||||
store.dispatch(BackupAction.FinishBackup)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SaltPay state maybe not initialized if backup resumed from ResumeFoundUnfinishedBackup action
|
||||
*/
|
||||
private fun initSaltPayOnBackupFinishedIfNeeded(
|
||||
scanResponse: ScanResponse?,
|
||||
onboardingWalletState: OnboardingWalletState,
|
||||
) {
|
||||
if (onboardingWalletState.isSaltPay && onboardingWalletState.onboardingSaltPayState == null) {
|
||||
//TODO: SaltPay: scanResponse
|
||||
if (scanResponse == null) throw IllegalArgumentException()
|
||||
|
||||
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
|
||||
store.dispatchOnMain(OnboardingSaltPayAction.Init.SetDependencies(manager, config))
|
||||
store.dispatchOnMain(OnboardingSaltPayAction.Update)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,13 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
|
|||
import com.tangem.tap.backupService
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayReducer
|
||||
import org.rekotlin.Action
|
||||
|
||||
class OnboardingWalletReducer {
|
||||
companion object {
|
||||
fun reduce(action: Action, state: AppState): OnboardingWalletState =
|
||||
internalReduce(action, state)
|
||||
fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -16,13 +17,17 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
|
|||
val state = appState.onboardingWalletState
|
||||
|
||||
return when (action) {
|
||||
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState))
|
||||
is GlobalAction.Onboarding.Start -> OnboardingWalletState(
|
||||
backupState = state.backupState.copy(canSkipBackup = action.fromHomeScreen)
|
||||
is GlobalAction.Onboarding -> ReducerForGlobalAction.reduce(action, state)
|
||||
is OnboardingSaltPayAction -> OnboardingSaltPayReducer.reduce(action, state)
|
||||
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState, state.isSaltPay))
|
||||
|
||||
is OnboardingWalletAction.GetToCreateWalletStep -> OnboardingWalletState(
|
||||
step = OnboardingWalletStep.CreateWallet,
|
||||
)
|
||||
is OnboardingWalletAction.GetToCreateWalletStep ->
|
||||
OnboardingWalletState(step = OnboardingWalletStep.CreateWallet)
|
||||
is OnboardingWalletAction.ProceedBackup -> state.copy(step = OnboardingWalletStep.Backup)
|
||||
is OnboardingWalletAction.GetToSaltPayStep -> state.copy(
|
||||
step = OnboardingWalletStep.SaltPay,
|
||||
)
|
||||
is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup)
|
||||
is OnboardingWalletAction.SetArtworkUrl -> {
|
||||
state.copy(cardArtworkUrl = action.artworkUrl)
|
||||
}
|
||||
|
|
@ -31,18 +36,45 @@ 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()
|
||||
)
|
||||
}
|
||||
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
|
||||
OnboardingWalletState(
|
||||
backupState = state.backupState.copy(
|
||||
maxBackupCards = if (action.isSaltPayBackup) 1 else 2,
|
||||
canSkipBackup = false,
|
||||
),
|
||||
isSaltPay = action.isSaltPayBackup,
|
||||
)
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BackupReducer {
|
||||
private class BackupReducer {
|
||||
companion object {
|
||||
fun reduce(
|
||||
action: BackupAction, state: BackupState,
|
||||
action: BackupAction,
|
||||
state: BackupState,
|
||||
isSaltPay: Boolean,
|
||||
): BackupState {
|
||||
|
||||
return when (action) {
|
||||
is BackupAction.IntroduceBackup -> BackupState(
|
||||
backupStep = BackupStep.InitBackup,
|
||||
canSkipBackup = state.canSkipBackup,
|
||||
isSaltPay = action.isSaltPay,
|
||||
maxBackupCards = if (isSaltPay) 1 else 2,
|
||||
canSkipBackup = !isSaltPay,
|
||||
)
|
||||
BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard)
|
||||
BackupAction.StartAddingBackupCards -> {
|
||||
|
|
@ -51,9 +83,6 @@ class BackupReducer {
|
|||
BackupAction.AddBackupCard.Success -> {
|
||||
state.copy(backupCardsNumber = state.backupCardsNumber + 1)
|
||||
}
|
||||
BackupAction.FinishAddingBackupCards -> {
|
||||
state
|
||||
}
|
||||
BackupAction.ShowAccessCodeInfoScreen -> {
|
||||
state.copy(backupStep = BackupStep.SetAccessCode)
|
||||
}
|
||||
|
|
@ -63,13 +92,11 @@ class BackupReducer {
|
|||
is BackupAction.SaveFirstAccessCode -> {
|
||||
state.copy(
|
||||
backupStep = BackupStep.ReenterAccessCode,
|
||||
accessCode = action.accessCode
|
||||
accessCode = action.accessCode,
|
||||
)
|
||||
}
|
||||
is BackupAction.SetAccessCodeError -> {
|
||||
state.copy(
|
||||
accessCodeError = action.error
|
||||
)
|
||||
state.copy(accessCodeError = action.error)
|
||||
}
|
||||
is BackupAction.PrepareToWritePrimaryCard -> {
|
||||
if (state.primaryCardId == null) {
|
||||
|
|
@ -77,12 +104,10 @@ class BackupReducer {
|
|||
primaryCardId = backupService.primaryCardId,
|
||||
backupCardIds = backupService.backupCardIds,
|
||||
backupCardsNumber = backupService.backupCardIds.size,
|
||||
backupStep = BackupStep.WritePrimaryCard
|
||||
backupStep = BackupStep.WritePrimaryCard,
|
||||
)
|
||||
} else {
|
||||
state.copy(
|
||||
backupStep = BackupStep.WritePrimaryCard
|
||||
)
|
||||
state.copy(backupStep = BackupStep.WritePrimaryCard)
|
||||
}
|
||||
}
|
||||
is BackupAction.PrepareToWriteBackupCard -> {
|
||||
|
|
@ -91,43 +116,21 @@ class BackupReducer {
|
|||
primaryCardId = backupService.primaryCardId,
|
||||
backupCardIds = backupService.backupCardIds,
|
||||
backupCardsNumber = backupService.backupCardIds.size,
|
||||
backupStep = BackupStep.WriteBackupCard(action.cardNumber)
|
||||
backupStep = BackupStep.WriteBackupCard(action.cardNumber),
|
||||
)
|
||||
} else {
|
||||
state.copy(
|
||||
backupStep = BackupStep.WriteBackupCard(action.cardNumber)
|
||||
)
|
||||
state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber))
|
||||
}
|
||||
}
|
||||
|
||||
is BackupAction.FinishBackup -> {
|
||||
state.copy(
|
||||
backupStep = BackupStep.Finished
|
||||
)
|
||||
state.copy(backupStep = BackupStep.Finished)
|
||||
}
|
||||
|
||||
BackupAction.OnAccessCodeDialogClosed -> {
|
||||
state.copy(backupStep = BackupStep.AddBackupCards)
|
||||
}
|
||||
BackupAction.DiscardBackup -> BackupState()
|
||||
|
||||
BackupAction.WritePrimaryCard -> state
|
||||
is BackupAction.WriteBackupCard -> state
|
||||
is BackupAction.SaveAccessCodeConfirmation -> state
|
||||
BackupAction.ScanPrimaryCard -> state
|
||||
BackupAction.ShowEnterAccessCodeScreen -> state
|
||||
BackupAction.ShowReenterAccessCodeScreen -> state
|
||||
BackupAction.AddBackupCard -> state
|
||||
BackupAction.DismissBackup -> state
|
||||
is BackupAction.LoadBackupCardArtwork -> state
|
||||
is BackupAction.CheckAccessCode -> state
|
||||
BackupAction.DetermineBackupStep -> state
|
||||
BackupAction.CheckForUnfinishedBackup -> state
|
||||
BackupAction.ResumeBackup -> state
|
||||
BackupAction.DiscardSavedBackup -> state
|
||||
BackupAction.StartBackup -> state
|
||||
else -> state
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
|
|||
|
||||
import android.graphics.Bitmap
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayRegistrationStep
|
||||
import org.rekotlin.StateType
|
||||
|
||||
/**
|
||||
|
|
@ -10,15 +12,20 @@ import org.rekotlin.StateType
|
|||
data class OnboardingWalletState(
|
||||
val step: OnboardingWalletStep = OnboardingWalletStep.None,
|
||||
val backupState: BackupState = BackupState(),
|
||||
val onboardingSaltPayState: OnboardingSaltPayState? = null,
|
||||
val isSaltPay: Boolean = false,
|
||||
val cardArtworkUrl: String? = null,
|
||||
val showConfetti: Boolean = false,
|
||||
) : StateType {
|
||||
|
||||
fun getMaxProgress(): Int = when {
|
||||
isSaltPay -> 12
|
||||
else -> 6
|
||||
}
|
||||
|
||||
fun getProgressStep(): Int {
|
||||
return when {
|
||||
step == OnboardingWalletStep.CreateWallet -> {
|
||||
1
|
||||
}
|
||||
step == OnboardingWalletStep.CreateWallet -> 1
|
||||
step == OnboardingWalletStep.Backup -> {
|
||||
when (backupState.backupStep) {
|
||||
BackupStep.InitBackup -> 2
|
||||
|
|
@ -29,30 +36,41 @@ data class OnboardingWalletState(
|
|||
BackupStep.ReenterAccessCode -> 4
|
||||
BackupStep.SetAccessCode -> 4
|
||||
BackupStep.WritePrimaryCard, is BackupStep.WriteBackupCard -> 5
|
||||
BackupStep.Finished -> 6
|
||||
BackupStep.Finished -> getMaxProgress()
|
||||
}
|
||||
}
|
||||
step == OnboardingWalletStep.Done -> 6
|
||||
step == OnboardingWalletStep.SaltPay && isSaltPay -> {
|
||||
when (onboardingSaltPayState!!.step) {
|
||||
SaltPayRegistrationStep.NoGas -> 1
|
||||
SaltPayRegistrationStep.NeedPin -> 7
|
||||
SaltPayRegistrationStep.CardRegistration -> 8
|
||||
SaltPayRegistrationStep.Kyc -> 9
|
||||
SaltPayRegistrationStep.KycStart -> 10
|
||||
SaltPayRegistrationStep.KycWaiting -> 11
|
||||
SaltPayRegistrationStep.Finished -> getMaxProgress()
|
||||
}
|
||||
}
|
||||
step == OnboardingWalletStep.Done -> getMaxProgress()
|
||||
else -> 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class OnboardingWalletStep {
|
||||
None, CreateWallet, Backup, Done
|
||||
None, CreateWallet, Backup, SaltPay, Done
|
||||
}
|
||||
|
||||
data class BackupState(
|
||||
val primaryCardId: String? = null,
|
||||
val backupCardsNumber: Int = 0,
|
||||
val backupCardIds: List<CardId> = emptyList(),
|
||||
@Transient
|
||||
val backupCardsArtworks: Map<CardId, Bitmap> = emptyMap(),
|
||||
val accessCode: String = "",
|
||||
val accessCodeError: AccessCodeError? = null,
|
||||
val backupStep: BackupStep = BackupStep.InitBackup,
|
||||
val maxBackupCards: Int = 2,
|
||||
val canSkipBackup: Boolean = true,
|
||||
val isSaltPay: Boolean = false,
|
||||
)
|
||||
|
||||
enum class AccessCodeError {
|
||||
|
|
@ -73,7 +91,7 @@ sealed class BackupStep {
|
|||
object Finished : BackupStep()
|
||||
}
|
||||
|
||||
sealed class BackupDialog: StateDialog {
|
||||
sealed class BackupDialog : StateDialog {
|
||||
object AddMoreBackupCards : StateDialog
|
||||
object BackupInProgress : StateDialog
|
||||
object UnfinishedBackupFound : StateDialog
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import com.tangem.blockchain.blockchains.ethereum.CompiledEthereumTransaction
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.extensions.successOr
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.domain.common.extensions.successOr
|
||||
import com.tangem.tap.common.extensions.isPositive
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.common.extensions.stub
|
||||
import com.tangem.tap.domain.getFirstToken
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class GnosisRegistrator(
|
||||
val walletManager: EthereumWalletManager,
|
||||
) {
|
||||
|
||||
private val walletAddress = walletManager.wallet.address
|
||||
private val token = walletManager.wallet.getFirstToken().guard {
|
||||
throw NullPointerException("WalletManager for GnosisRegistrator must contain token")
|
||||
}
|
||||
|
||||
private val otpProcessorContractAddress: String = when (walletManager.wallet.blockchain) {
|
||||
Blockchain.SaltPay -> "0x3B4397C817A26521Df8bD01a949AFDE2251d91C2"
|
||||
Blockchain.SaltPayTestnet -> "0x710BF23486b549836509a08c184cE0188830f197"
|
||||
else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain")
|
||||
}
|
||||
|
||||
private val atomicNonce = AtomicLong()
|
||||
|
||||
suspend fun checkHasGas(): Result<Boolean> {
|
||||
val wallet = walletManager.safeUpdate().successOr {
|
||||
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
|
||||
}
|
||||
|
||||
val hasGas = wallet.amounts[AmountType.Coin]?.value?.isPositive() ?: false
|
||||
return Result.Success(hasGas)
|
||||
}
|
||||
|
||||
suspend fun prepareBeforeMakingTxs(): Result<Unit> {
|
||||
walletManager.safeUpdate().successOr {
|
||||
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
|
||||
}
|
||||
atomicNonce.set(walletManager.txCount)
|
||||
|
||||
return Result.Success(Unit)
|
||||
}
|
||||
|
||||
suspend fun sendTransactions(transactions: List<SignedEthereumTransaction>): Result<List<String>> {
|
||||
val results = transactions.map {
|
||||
coroutineScope { async { walletManager.sendRaw(it.compiledTransaction, it.signature) } }
|
||||
}.awaitAll()
|
||||
|
||||
val errors = results.filterIsInstance<SimpleResult.Failure>()
|
||||
return if (errors.isEmpty()) {
|
||||
Result.Success(listOf())
|
||||
} else {
|
||||
Result.Failure(errors[0].error)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun makeSetSpendLimitTx(value: BigDecimal): Result<CompiledEthereumTransaction> {
|
||||
val limitAmount = Amount(walletManager.cardTokens.first(), value)
|
||||
val feeAmount = walletManager.getFeeToSetSpendLimit(otpProcessorContractAddress, limitAmount)
|
||||
.extractFeeAmount().successOr { return it }
|
||||
|
||||
val compiledEthereumTransaction = walletManager.transactionBuilder.buildSetSpendLimitToSign(
|
||||
processorContractAddress = otpProcessorContractAddress,
|
||||
cardAddress = walletAddress,
|
||||
amount = limitAmount,
|
||||
transactionFee = feeAmount,
|
||||
gasLimit = walletManager.gasLimitToSetSpendLimit,
|
||||
nonce = atomicNonce.getAndIncrement().toBigInteger(),
|
||||
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'setSpendLimit' transaction"))
|
||||
|
||||
return Result.Success(compiledEthereumTransaction)
|
||||
}
|
||||
|
||||
suspend fun makeInitOtpTx(rootOTP: ByteArray, rootOTPCounter: Int): Result<CompiledEthereumTransaction> {
|
||||
val feeAmount = walletManager.getFeeToInitOTP(otpProcessorContractAddress, rootOTP, rootOTPCounter)
|
||||
.extractFeeAmount().successOr { return it }
|
||||
|
||||
val compiledEthereumTransaction = walletManager.transactionBuilder.buildInitOTPToSign(
|
||||
processorContractAddress = otpProcessorContractAddress,
|
||||
cardAddress = walletAddress,
|
||||
otp = rootOTP,
|
||||
otpCounter = rootOTPCounter,
|
||||
transactionFee = feeAmount,
|
||||
gasLimit = walletManager.gasLimitToInitOTP,
|
||||
nonce = atomicNonce.getAndIncrement().toBigInteger(),
|
||||
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'initOtp' transaction"))
|
||||
|
||||
return Result.Success(compiledEthereumTransaction)
|
||||
}
|
||||
|
||||
suspend fun makeSetWalletTx(): Result<CompiledEthereumTransaction> {
|
||||
val feeAmount = walletManager.getFeeToSetWallet(otpProcessorContractAddress)
|
||||
.extractFeeAmount().successOr { return it }
|
||||
|
||||
val compiledEthereumTransaction = walletManager.transactionBuilder.buildSetWalletToSign(
|
||||
processorContractAddress = otpProcessorContractAddress,
|
||||
cardAddress = walletAddress,
|
||||
transactionFee = feeAmount,
|
||||
gasLimit = walletManager.gasLimitToSetWallet,
|
||||
nonce = atomicNonce.getAndIncrement().toBigInteger(),
|
||||
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'setWallet' transaction"))
|
||||
|
||||
return Result.Success(compiledEthereumTransaction)
|
||||
}
|
||||
|
||||
suspend fun makeApprovalTx(value: BigDecimal): Result<CompiledEthereumTransaction> {
|
||||
val approveAmount = Amount(token, value)
|
||||
val feeAmount = walletManager.getFeeToApprove(approveAmount, otpProcessorContractAddress)
|
||||
.extractFeeAmount().successOr { return it }
|
||||
|
||||
val compiledEthereumTransaction = walletManager.transactionBuilder.buildApproveToSign(
|
||||
transactionData = walletManager.createTransaction(
|
||||
amount = approveAmount,
|
||||
fee = feeAmount,
|
||||
destination = otpProcessorContractAddress,
|
||||
),
|
||||
nonce = atomicNonce.getAndIncrement().toBigInteger(),
|
||||
gasLimit = walletManager.gasLimitToApprove,
|
||||
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'approval' transaction"))
|
||||
|
||||
return Result.Success(compiledEthereumTransaction)
|
||||
}
|
||||
|
||||
private fun Result<List<Amount>>.extractFeeAmount(): Result<Amount> {
|
||||
return when (this) {
|
||||
is Result.Success -> {
|
||||
if (this.data.size != 3) Result.Failure(BlockchainSdkError.FailedToLoadFee)
|
||||
else Result.Success(this.data[1])
|
||||
}
|
||||
is Result.Failure -> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long?.toBigInteger(): BigInteger? = this?.let { BigInteger.valueOf(it) }
|
||||
|
||||
private fun Long.toBigInteger(): BigInteger = BigInteger.valueOf(this)
|
||||
|
||||
companion object {
|
||||
fun stub(): GnosisRegistrator {
|
||||
return GnosisRegistrator(WalletManager.stub())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import com.tangem.common.Filter
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class PinFilter : Filter<String>
|
||||
|
||||
class AllSymbolsTheSameFilter : PinFilter() {
|
||||
override fun filter(value: String): Boolean {
|
||||
if (value.isEmpty()) return false
|
||||
|
||||
val array = value.toCharArray()
|
||||
val firstSymbol = array[0]
|
||||
return array.all { it == firstSymbol }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class SaltPayConfig(
|
||||
val zendesk: ZendeskConfig,
|
||||
val kycProvider: KYCProvider,
|
||||
) {
|
||||
companion object {
|
||||
fun stub(): SaltPayConfig {
|
||||
return SaltPayConfig(
|
||||
zendesk = ZendeskConfig("", "", "", "", ""),
|
||||
kycProvider = KYCProvider("", "", "", ""),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class KYCProvider(
|
||||
val baseUrl: String,
|
||||
val externalIdParameterKey: String,
|
||||
val sidParameterKey: String,
|
||||
val sidValue: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import android.net.Uri
|
||||
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.extensions.successOr
|
||||
import com.tangem.network.api.paymentology.AttestationResponse
|
||||
import com.tangem.network.api.paymentology.PaymentologyApiService
|
||||
import com.tangem.network.api.paymentology.RegisterKYCRequest
|
||||
import com.tangem.network.api.paymentology.RegisterWalletRequest
|
||||
import com.tangem.network.api.paymentology.RegisterWalletResponse
|
||||
import com.tangem.network.api.paymentology.RegistrationResponse
|
||||
import com.tangem.operations.attestation.AttestWalletKeyResponse
|
||||
import com.tangem.tap.domain.tokens.UserWalletId
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayRegistrationError
|
||||
import com.tangem.tap.persistence.SaltPayRegistrationStorage
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class SaltPayRegistrationManager(
|
||||
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)
|
||||
|
||||
private val approvalValue: BigDecimal = BigDecimal.ONE
|
||||
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)
|
||||
}
|
||||
is com.tangem.blockchain.extensions.Result.Failure -> Result.Failure(SaltPayRegistrationError.NoGas)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun registerKYC(): Result<Unit> {
|
||||
return when (val result = paymentologyService.registerKYC(makeRegisterKYCRequest())) {
|
||||
is Result.Success -> Result.Success(Unit)
|
||||
is Result.Failure -> result
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkRegistration(): Result<RegistrationResponse.Item> {
|
||||
val registrationResponse = paymentologyService.checkRegistration(cardId, cardPublicKey).successOr { return it }
|
||||
|
||||
return try {
|
||||
if (!registrationResponse.success || registrationResponse.results.isEmpty()) {
|
||||
throw SaltPayRegistrationError.EmptyResponse
|
||||
}
|
||||
Result.Success(registrationResponse.results[0])
|
||||
} catch (ex: SaltPayRegistrationError) {
|
||||
Result.Failure(ex)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requestAttestationChallenge(): Result<AttestationResponse> {
|
||||
return paymentologyService.requestAttestationChallenge(cardId, cardPublicKey)
|
||||
}
|
||||
|
||||
suspend fun sendTransactions(
|
||||
signedTransactions: List<SignedEthereumTransaction>,
|
||||
): com.tangem.blockchain.extensions.Result<List<String>> {
|
||||
return gnosisRegistrator.sendTransactions(signedTransactions)
|
||||
}
|
||||
|
||||
suspend fun registerWallet(
|
||||
attestResponse: AttestWalletKeyResponse,
|
||||
pinCode: String,
|
||||
): Result<RegisterWalletResponse> {
|
||||
val request = RegisterWalletRequest(
|
||||
cardId = cardId,
|
||||
publicKey = cardPublicKey,
|
||||
walletPublicKey = walletPublicKey,
|
||||
walletSalt = attestResponse.salt,
|
||||
walletSignature = attestResponse.walletSignature,
|
||||
cardSalt = attestResponse.publicKeySalt ?: byteArrayOf(),
|
||||
cardSignature = attestResponse.cardSignature ?: byteArrayOf(),
|
||||
pin = pinCode,
|
||||
)
|
||||
|
||||
val result = paymentologyService.registerWallet(request)
|
||||
|
||||
registrationStorage.data = registrationStorage.data.copy(
|
||||
transactionsSent = result is Result.Success,
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun makeRegistrationTask(challenge: ByteArray): SaltPayRegistrationTask {
|
||||
return SaltPayRegistrationTask(
|
||||
gnosisRegistrator = gnosisRegistrator,
|
||||
challenge = challenge,
|
||||
walletPublicKey = walletPublicKey,
|
||||
approvalValue = approvalValue,
|
||||
spendLimitValue = spendLimitValue,
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeRegisterKYCRequest(): RegisterKYCRequest = RegisterKYCRequest(
|
||||
cardId = cardId,
|
||||
publicKey = cardPublicKey,
|
||||
kycProvider = "UTORG",
|
||||
kycRefId = kycUrlProvider.kycRefId,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun stub(): SaltPayRegistrationManager = SaltPayRegistrationManager(
|
||||
kycProvider = SaltPayConfig.stub().kycProvider,
|
||||
paymentologyService = PaymentologyApiService.stub(),
|
||||
gnosisRegistrator = GnosisRegistrator.stub(),
|
||||
registrationStorage = SaltPayRegistrationStorage.stub(),
|
||||
cardId = "",
|
||||
cardPublicKey = byteArrayOf(),
|
||||
walletPublicKey = byteArrayOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class KYCUrlProvider(
|
||||
walletPublicKey: ByteArray,
|
||||
kycProvider: KYCProvider,
|
||||
) {
|
||||
|
||||
val kycRefId = UserWalletId(walletPublicKey).stringValue
|
||||
|
||||
val requestUrl: String = makeWebRequestUrl(kycProvider)
|
||||
|
||||
val doneUrl = "https://success.tangem.com/"
|
||||
|
||||
private fun makeWebRequestUrl(kycProvider: KYCProvider): String {
|
||||
val baseUri = Uri.parse(kycProvider.baseUrl)
|
||||
return Uri.Builder()
|
||||
.scheme(baseUri.scheme)
|
||||
.authority(baseUri.authority)
|
||||
.encodedPath(baseUri.path)
|
||||
.appendQueryParameter(kycProvider.sidParameterKey, kycProvider.sidValue)
|
||||
.appendQueryParameter(kycProvider.externalIdParameterKey, kycRefId)
|
||||
.build().toString()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay
|
||||
|
||||
import com.tangem.blockchain.blockchains.ethereum.CompiledEthereumTransaction
|
||||
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
|
||||
import com.tangem.blockchain.extensions.successOr
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.CompletionCallback
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.operations.CommandResponse
|
||||
import com.tangem.operations.GenerateOTPCommand
|
||||
import com.tangem.operations.GenerateOTPResponse
|
||||
import com.tangem.operations.attestation.AttestWalletKeyCommand
|
||||
import com.tangem.operations.attestation.AttestWalletKeyResponse
|
||||
import com.tangem.operations.sign.SignHashesCommand
|
||||
import com.tangem.tap.scope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class RegistrationTaskResponse(
|
||||
val signedTransactions: List<SignedEthereumTransaction>,
|
||||
val attestResponse: AttestWalletKeyResponse,
|
||||
) : CommandResponse
|
||||
|
||||
class SaltPayRegistrationTask(
|
||||
private var gnosisRegistrator: GnosisRegistrator,
|
||||
private var challenge: ByteArray,
|
||||
private val walletPublicKey: ByteArray,
|
||||
private val approvalValue: BigDecimal,
|
||||
private val spendLimitValue: BigDecimal,
|
||||
) : CardSessionRunnable<RegistrationTaskResponse> {
|
||||
|
||||
private var generateOTPResponse: GenerateOTPResponse? = null
|
||||
private var attestWalletResponse: AttestWalletKeyResponse? = null
|
||||
private var signedTransactions: List<SignedEthereumTransaction> = listOf()
|
||||
|
||||
override fun run(session: CardSession, callback: CompletionCallback<RegistrationTaskResponse>) {
|
||||
generateOTP(session, callback)
|
||||
}
|
||||
|
||||
private fun generateOTP(session: CardSession, callback: CompletionCallback<RegistrationTaskResponse>) {
|
||||
GenerateOTPCommand().run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
generateOTPResponse = result.data
|
||||
attestWallet(session, callback)
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attestWallet(
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<RegistrationTaskResponse>,
|
||||
) {
|
||||
val card = session.environment.card.guard {
|
||||
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
|
||||
return
|
||||
}
|
||||
|
||||
val walletPublicKey = card.wallets.firstOrNull()?.publicKey
|
||||
if (walletPublicKey == null || !walletPublicKey.contentEquals(this.walletPublicKey)) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.WalletNotFound()))
|
||||
return
|
||||
}
|
||||
|
||||
val attestWalletCommand = AttestWalletKeyCommand(
|
||||
publicKey = walletPublicKey,
|
||||
challenge = challenge,
|
||||
confirmationMode = AttestWalletKeyCommand.ConfirmationMode.Dynamic,
|
||||
)
|
||||
attestWalletCommand.run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
attestWalletResponse = result.data
|
||||
scope.launch { prepareTransactions(session, callback) }
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prepareTransactions(
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<RegistrationTaskResponse>,
|
||||
) {
|
||||
val generateOTPResponse = this.generateOTPResponse.guard {
|
||||
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
|
||||
return
|
||||
}
|
||||
|
||||
gnosisRegistrator.prepareBeforeMakingTxs().successOr {
|
||||
callback(CompletionResult.Failure(it.error))
|
||||
return
|
||||
}
|
||||
|
||||
val approvalTx = gnosisRegistrator.makeApprovalTx(approvalValue).successOr {
|
||||
callback(CompletionResult.Failure(it.error))
|
||||
return
|
||||
}
|
||||
val setWalletTx = gnosisRegistrator.makeSetWalletTx().successOr {
|
||||
callback(CompletionResult.Failure(it.error))
|
||||
return
|
||||
}
|
||||
val initOtpTx = gnosisRegistrator.makeInitOtpTx(
|
||||
generateOTPResponse.rootOTP,
|
||||
generateOTPResponse.rootOTPCounter,
|
||||
).successOr {
|
||||
callback(CompletionResult.Failure(it.error))
|
||||
return
|
||||
}
|
||||
|
||||
val setSpendLimitTx = gnosisRegistrator.makeSetSpendLimitTx(spendLimitValue).successOr {
|
||||
callback(CompletionResult.Failure(it.error))
|
||||
return
|
||||
}
|
||||
|
||||
signTransactions(
|
||||
transactions = listOf(
|
||||
approvalTx,
|
||||
setWalletTx,
|
||||
initOtpTx,
|
||||
setSpendLimitTx,
|
||||
),
|
||||
session = session,
|
||||
callback = callback,
|
||||
)
|
||||
}
|
||||
|
||||
private fun signTransactions(
|
||||
transactions: List<CompiledEthereumTransaction>,
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<RegistrationTaskResponse>,
|
||||
) {
|
||||
val walletPublicKey = session.environment.card?.wallets?.firstOrNull()?.publicKey.guard {
|
||||
callback(CompletionResult.Failure(TangemSdkError.WalletNotFound()))
|
||||
return
|
||||
}
|
||||
|
||||
val hashes = transactions.map { it.hash }.toTypedArray()
|
||||
val signHashesCommand = SignHashesCommand(hashes, walletPublicKey)
|
||||
|
||||
signHashesCommand.run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
signedTransactions = transactions.zip(result.data.signatures).map {
|
||||
SignedEthereumTransaction(it.first, it.second)
|
||||
}
|
||||
complete(callback)
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun complete(callback: CompletionCallback<RegistrationTaskResponse>) {
|
||||
if (signedTransactions.isEmpty() || attestWalletResponse == null) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
|
||||
} else {
|
||||
val result = RegistrationTaskResponse(signedTransactions, attestWalletResponse!!)
|
||||
callback(CompletionResult.Success(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
|
||||
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
|
||||
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayRegistrationManager
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class OnboardingSaltPayAction : Action {
|
||||
sealed class Init : OnboardingSaltPayAction() {
|
||||
data class SetDependencies(
|
||||
val registrationManager: SaltPayRegistrationManager,
|
||||
val saltPayConfig: SaltPayConfig,
|
||||
) : Init()
|
||||
|
||||
// only for tests
|
||||
object DiscardBackupSteps : Init()
|
||||
}
|
||||
|
||||
data class SetIsBusy(val isBusy: Boolean) : OnboardingSaltPayAction()
|
||||
data class SetAccessCode(val accessCode: ByteArray?) : OnboardingSaltPayAction()
|
||||
|
||||
object Update : OnboardingSaltPayAction()
|
||||
object RegisterCard : OnboardingSaltPayAction()
|
||||
object KYCStart : OnboardingSaltPayAction()
|
||||
object OnFinishKYC : OnboardingSaltPayAction()
|
||||
|
||||
data class TrySetPin(val pin: String) : OnboardingSaltPayAction()
|
||||
data class SetPin(val pin: String) : OnboardingSaltPayAction()
|
||||
|
||||
// data class TryUpdateStep(val newStep: SaltPayRegistrationStep? = null) : OnboardingSaltPayAction()
|
||||
data class SetStep(val newStep: SaltPayRegistrationStep? = null) : OnboardingSaltPayAction()
|
||||
}
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
|
||||
|
||||
import com.tangem.Message
|
||||
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.RegistrationResponse
|
||||
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.redux.AppState
|
||||
import com.tangem.tap.domain.TapError
|
||||
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.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class OnboardingSaltPayMiddleware {
|
||||
companion object {
|
||||
val handler = onboardingSaltPayMiddleware
|
||||
}
|
||||
}
|
||||
|
||||
private val onboardingSaltPayMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
handleOnboardingSaltPayAction(action, state)
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> AppState?) {
|
||||
if (DemoHelper.tryHandle(appState, anyAction)) return
|
||||
val action = anyAction as? OnboardingSaltPayAction ?: return
|
||||
appState() ?: return
|
||||
|
||||
fun getAppState(): AppState = appState()!!
|
||||
fun getOnboardingWalletState(): OnboardingWalletState = getAppState().onboardingWalletState
|
||||
fun getState(): OnboardingSaltPayState = getOnboardingWalletState().onboardingSaltPayState!!
|
||||
|
||||
when (action) {
|
||||
is OnboardingSaltPayAction.RegisterCard -> {
|
||||
handleIsBusy = true
|
||||
val state = getState()
|
||||
|
||||
scope.launch {
|
||||
val saltPayManager = state.saltPayManager
|
||||
checkGasIfNeeded(state.saltPayManager, state.step).successOr {
|
||||
onException(it.error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val pinCode = state.pinCode.guard {
|
||||
onError(SaltPayRegistrationError.NeedPin)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val attestationResponse = saltPayManager.requestAttestationChallenge().successOr {
|
||||
onException(it.error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (!attestationResponse.success) {
|
||||
onError(SaltPayRegistrationError.CardNotFound)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val registrationResponse = tangemSdkManager.runTaskAsync(
|
||||
runnable = saltPayManager.makeRegistrationTask(attestationResponse.challenge!!),
|
||||
cardId = saltPayManager.cardId,
|
||||
initialMessage = Message(
|
||||
header = null,
|
||||
body = tangemSdkManager.getString(R.string.registration_task_alert_message),
|
||||
),
|
||||
).successOr {
|
||||
onException(it.error as Exception)
|
||||
return@launch
|
||||
}
|
||||
|
||||
saltPayManager.sendTransactions(registrationResponse.signedTransactions).successOr {
|
||||
onError(SaltPayRegistrationError.FailedToSendTx)
|
||||
return@launch
|
||||
}
|
||||
|
||||
saltPayManager.registerWallet(registrationResponse.attestResponse, pinCode).successOr {
|
||||
onException(it.error)
|
||||
return@launch
|
||||
}
|
||||
|
||||
withMainContext {
|
||||
handleIsBusy = false
|
||||
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.Kyc))
|
||||
}
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.KYCStart -> {
|
||||
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.KycStart))
|
||||
}
|
||||
is OnboardingSaltPayAction.OnFinishKYC -> {
|
||||
scope.launch {
|
||||
registerKYCIfNeeded(getState()).successOr {
|
||||
onException(it.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.TrySetPin -> {
|
||||
try {
|
||||
assertPinValid(action.pin, getState().pinLength)
|
||||
store.dispatch(OnboardingSaltPayAction.SetPin(action.pin))
|
||||
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayRegistrationStep.CardRegistration))
|
||||
} catch (error: SaltPayRegistrationError) {
|
||||
handleError = error
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.Update -> {
|
||||
handleIsBusy = true
|
||||
|
||||
val state = getState()
|
||||
scope.launch {
|
||||
checkGasIfNeeded(state.saltPayManager, state.step).successOr {
|
||||
onException(it.error)
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
/* do nothing, only reduce */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var handleIsBusy: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
store.dispatchOnMain(OnboardingSaltPayAction.SetIsBusy(value))
|
||||
}
|
||||
|
||||
private suspend fun onError(error: SaltPayRegistrationError) {
|
||||
withMainContext {
|
||||
handleIsBusy = false
|
||||
handleError = error
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun onException(error: Throwable) {
|
||||
withMainContext {
|
||||
handleIsBusy = false
|
||||
handleException = 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,
|
||||
): Result<Unit> {
|
||||
if (step == SaltPayRegistrationStep.KycStart ||
|
||||
step == SaltPayRegistrationStep.KycWaiting ||
|
||||
step == SaltPayRegistrationStep.Finished
|
||||
) {
|
||||
return Result.Success(Unit)
|
||||
}
|
||||
|
||||
return saltPayManager.checkHasGas()
|
||||
}
|
||||
|
||||
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 {
|
||||
return when {
|
||||
passed != true -> throw SaltPayRegistrationError.CardNotPassed
|
||||
disabledByAdmin == true -> throw SaltPayRegistrationError.CardDisabled
|
||||
|
||||
// go to success screen
|
||||
active == true -> SaltPayRegistrationStep.Finished
|
||||
|
||||
// pinSet is false, go toPin screen
|
||||
pinSet == false -> SaltPayRegistrationStep.NeedPin
|
||||
|
||||
// kycDate is set, go to kyc waiting screen
|
||||
kycDate != null -> SaltPayRegistrationStep.KycWaiting
|
||||
|
||||
else -> SaltPayRegistrationStep.KycStart
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SaltPayRegistrationError::class)
|
||||
private fun assertPinValid(pin: String, pinLength: Int) {
|
||||
val array = pin.toCharArray()
|
||||
|
||||
if (array.size < pinLength) throw SaltPayRegistrationError.WeakPin
|
||||
|
||||
val filters: List<Filter<String>> = listOf(
|
||||
AllSymbolsTheSameFilter(),
|
||||
)
|
||||
|
||||
if (filters.any { it.filter(pin) }) {
|
||||
throw SaltPayRegistrationError.WeakPin
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
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
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class OnboardingSaltPayReducer {
|
||||
companion object {
|
||||
fun reduce(action: Action, state: OnboardingWalletState): OnboardingWalletState {
|
||||
return internalReduce(action, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingWalletState): OnboardingWalletState {
|
||||
val action = anyAction as? OnboardingSaltPayAction ?: return onboardingWalletState
|
||||
|
||||
if (onboardingWalletState.onboardingSaltPayState == null && action is OnboardingSaltPayAction.Init.SetDependencies) {
|
||||
return onboardingWalletState.copy(
|
||||
onboardingSaltPayState = OnboardingSaltPayState(
|
||||
saltPayManager = action.registrationManager,
|
||||
saltPayConfig = action.saltPayConfig,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val oldState = onboardingWalletState.onboardingSaltPayState ?: return onboardingWalletState
|
||||
|
||||
val newState = when (action) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
is OnboardingSaltPayAction.SetPin -> {
|
||||
oldState.copy(pinCode = action.pin)
|
||||
}
|
||||
is OnboardingSaltPayAction.SetStep -> {
|
||||
if (action.newStep == null) {
|
||||
oldState
|
||||
} else {
|
||||
oldState.copy(
|
||||
step = action.newStep,
|
||||
)
|
||||
}
|
||||
}
|
||||
is OnboardingSaltPayAction.SetIsBusy -> {
|
||||
oldState.copy(
|
||||
isBusy = action.isBusy,
|
||||
)
|
||||
}
|
||||
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) {
|
||||
oldState -> onboardingWalletState
|
||||
else -> {
|
||||
onboardingWalletState.copy(onboardingSaltPayState = newState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
|
||||
|
||||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.network.api.paymentology.PaymentologyApiService
|
||||
import com.tangem.tap.common.toggleWidget.WidgetState
|
||||
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.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
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class OnboardingSaltPayState(
|
||||
@Transient
|
||||
val saltPayManager: SaltPayRegistrationManager = SaltPayRegistrationManager.stub(),
|
||||
val saltPayConfig: SaltPayConfig = SaltPayConfig.stub(),
|
||||
val pinCode: String? = null,
|
||||
val accessCode: ByteArray? = null,
|
||||
val step: SaltPayRegistrationStep = SaltPayRegistrationStep.NeedPin,
|
||||
val saltPayCardArtworkUrl: String? = null,
|
||||
val isBusy: Boolean = false,
|
||||
val isTest: Boolean = false,
|
||||
) {
|
||||
|
||||
val mainButtonState: WidgetState
|
||||
get() = if (isBusy) ProgressState.Loading else ProgressState.Done
|
||||
|
||||
val pinLength: Int = 4
|
||||
|
||||
companion object {
|
||||
fun initDependency(scanResponse: ScanResponse): Pair<SaltPayRegistrationManager, SaltPayConfig> {
|
||||
val globalState = store.state.globalState
|
||||
val saltPayConfig = globalState.configManager?.config?.saltPayConfig.guard {
|
||||
throw NullPointerException("SaltPayConfig is not initialized")
|
||||
}
|
||||
val gnosisRegistrator = makeGnosisRegistrator(
|
||||
scanResponse = scanResponse,
|
||||
wmFactory = globalState.tapWalletManager.walletManagerFactory,
|
||||
)
|
||||
val registrationManager = makeSaltPayRegistrationManager(
|
||||
scanResponse = scanResponse,
|
||||
gnosisRegistrator = gnosisRegistrator,
|
||||
paymentologyService = store.state.domainNetworks.paymentologyService,
|
||||
registrationStorage = preferencesStorage.saltPayRegistrationStorage,
|
||||
kycProvider = saltPayConfig.kycProvider,
|
||||
)
|
||||
return registrationManager to saltPayConfig
|
||||
}
|
||||
|
||||
fun makeSaltPayRegistrationManager(
|
||||
scanResponse: ScanResponse,
|
||||
gnosisRegistrator: GnosisRegistrator,
|
||||
paymentologyService: PaymentologyApiService,
|
||||
registrationStorage: SaltPayRegistrationStorage,
|
||||
kycProvider: KYCProvider,
|
||||
): SaltPayRegistrationManager {
|
||||
if (!scanResponse.isSaltPay()) {
|
||||
throw IllegalArgumentException("Can't initialize the OnboardingSaltPayMiddleware if card is not SalPay")
|
||||
}
|
||||
val saltPaySingleWallet = scanResponse.card.wallets.firstOrNull().guard {
|
||||
throw NullPointerException("SaltPay card must have one wallet at least")
|
||||
}
|
||||
|
||||
return SaltPayRegistrationManager(
|
||||
cardId = scanResponse.card.cardId,
|
||||
cardPublicKey = scanResponse.card.cardPublicKey,
|
||||
walletPublicKey = saltPaySingleWallet.publicKey,
|
||||
kycProvider = kycProvider,
|
||||
paymentologyService = paymentologyService,
|
||||
gnosisRegistrator = gnosisRegistrator,
|
||||
registrationStorage = registrationStorage,
|
||||
)
|
||||
}
|
||||
|
||||
fun makeGnosisRegistrator(
|
||||
scanResponse: ScanResponse,
|
||||
wmFactory: WalletManagerFactory,
|
||||
): GnosisRegistrator {
|
||||
return GnosisRegistrator(
|
||||
walletManager = wmFactory.makeSaltPayWalletManager(scanResponse),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SaltPayRegistrationStep {
|
||||
NoGas,
|
||||
NeedPin,
|
||||
CardRegistration,
|
||||
Kyc,
|
||||
KycStart,
|
||||
KycWaiting,
|
||||
Finished;
|
||||
}
|
||||
|
|
@ -1,25 +1,40 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.ui
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
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.transition.TransitionInflater
|
||||
import androidx.transition.TransitionManager
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import coil.load
|
||||
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.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.toggleWidget.IndeterminateProgressButtonWidget
|
||||
import com.tangem.tap.features.FragmentOnBackPressedHandler
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
|
||||
|
|
@ -28,24 +43,31 @@ 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.ui.dialogs.AccessCodeDialog
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
|
||||
import com.tangem.wallet.databinding.LayoutOnboardingSaltpayBinding
|
||||
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
||||
StoreSubscriber<OnboardingWalletState>, FragmentOnBackPressedHandler {
|
||||
|
||||
private var accessCodeDialog: AccessCodeDialog? = null
|
||||
private lateinit var cardsWidget: WalletCardsWidget
|
||||
private val binding: FragmentOnboardingWalletBinding by viewBinding(
|
||||
FragmentOnboardingWalletBinding::bind
|
||||
)
|
||||
private val pbBinding: ViewOnboardingProgressBinding by viewBinding(
|
||||
ViewOnboardingProgressBinding::bind)
|
||||
private val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
|
||||
private val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind)
|
||||
private val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer }
|
||||
|
||||
private val saltPayView: SaltPayView = SaltPayView(this)
|
||||
|
||||
private lateinit var cardsWidget: WalletCardsWidget
|
||||
private var accessCodeDialog: AccessCodeDialog? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -74,19 +96,19 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
binding.viewPagerBackupInfo.adapter = BackupInfoAdapter()
|
||||
TabLayoutMediator(
|
||||
binding.tabLayoutBackupInfo,
|
||||
binding.viewPagerBackupInfo
|
||||
binding.viewPagerBackupInfo,
|
||||
) { tab, position ->
|
||||
//Some implementation
|
||||
}.attach()
|
||||
|
||||
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
|
||||
|
||||
store.dispatch(OnboardingWalletAction.Init)
|
||||
binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
|
||||
|
||||
store.dispatch(OnboardingWalletAction.LoadArtwork)
|
||||
|
||||
addBackPressHandler(this)
|
||||
|
||||
store.dispatch(OnboardingWalletAction.Init)
|
||||
store.dispatch(OnboardingWalletAction.StartSaltPayCardActivation)
|
||||
store.dispatch(OnboardingWalletAction.LoadArtwork)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
|
@ -108,23 +130,16 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
|
||||
with(binding) {
|
||||
loadImageIntoImageView(state.cardArtworkUrl, imvFrontCard)
|
||||
loadImageIntoImageView(state.cardArtworkUrl, imvFirstBackupCard)
|
||||
loadImageIntoImageView(state.cardArtworkUrl, imvSecondBackupCard)
|
||||
}
|
||||
|
||||
pbBinding.pbState.max = 6
|
||||
pbBinding.pbState.max = state.getMaxProgress()
|
||||
pbBinding.pbState.progress = state.getProgressStep()
|
||||
|
||||
when (state.step) {
|
||||
OnboardingWalletStep.None -> {
|
||||
}
|
||||
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
|
||||
OnboardingWalletStep.Backup -> setBackupState(state.backupState)
|
||||
OnboardingWalletStep.Done -> {
|
||||
|
||||
}
|
||||
if (state.isSaltPay) {
|
||||
saltPayView.newState(state)
|
||||
} else {
|
||||
loadImageIntoImageView(state.cardArtworkUrl, binding.imvFrontCard)
|
||||
loadImageIntoImageView(state.cardArtworkUrl, binding.imvFirstBackupCard)
|
||||
loadImageIntoImageView(state.cardArtworkUrl, binding.imvSecondBackupCard)
|
||||
handleOnboardingStep(state)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,6 +151,14 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleOnboardingStep(state: OnboardingWalletState) {
|
||||
when (state.step) {
|
||||
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
|
||||
OnboardingWalletStep.Backup -> setBackupState(state.backupState, state.isSaltPay)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCreateWalletState() = with(binding) {
|
||||
layoutButtonsCommon.btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
|
||||
layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) }
|
||||
|
|
@ -149,12 +172,11 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
cardsWidget.toFolded(false) { startPostponedEnterTransition() }
|
||||
}
|
||||
|
||||
|
||||
private fun setBackupState(state: BackupState) {
|
||||
private fun setBackupState(state: BackupState, isSaltPay: Boolean) {
|
||||
when (state.backupStep) {
|
||||
BackupStep.InitBackup -> showBackupIntro(state)
|
||||
BackupStep.InitBackup -> showBackupIntro(state, isSaltPay)
|
||||
BackupStep.ScanOriginCard -> showScanOriginCard()
|
||||
BackupStep.AddBackupCards -> showAddBackupCards(state)
|
||||
BackupStep.AddBackupCards -> showAddBackupCards(state, isSaltPay)
|
||||
BackupStep.SetAccessCode -> showSetAccessCode()
|
||||
BackupStep.EnterAccessCode -> showEnterAccessCode(state)
|
||||
BackupStep.ReenterAccessCode -> showReenterAccessCode(state)
|
||||
|
|
@ -164,7 +186,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
}
|
||||
}
|
||||
|
||||
private fun showBackupIntro(state: BackupState) = with(binding) {
|
||||
private fun showBackupIntro(state: BackupState, isSaltPay: Boolean) = with(binding) {
|
||||
imvFirstBackupCard.show()
|
||||
imvSecondBackupCard.show()
|
||||
|
||||
|
|
@ -177,12 +199,11 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
|
||||
with(layoutButtonsCommon) {
|
||||
btnMainAction.text = getText(R.string.onboarding_button_backup_now)
|
||||
btnAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
|
||||
btnAlternativeAction.show(state.canSkipBackup)
|
||||
btnMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
|
||||
btnAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) }
|
||||
|
||||
btnAlternativeAction.show(!state.isSaltPay)
|
||||
btnAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
|
||||
btnAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) }
|
||||
btnAlternativeAction.show(state.canSkipBackup && !isSaltPay)
|
||||
}
|
||||
|
||||
startPostponedEnterTransition()
|
||||
|
|
@ -217,7 +238,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
imvSecondBackupCard.show()
|
||||
}
|
||||
|
||||
private fun showAddBackupCards(state: BackupState) = with(binding) {
|
||||
private fun showAddBackupCards(state: BackupState, isSaltPay: Boolean) = with(binding) {
|
||||
prepareBackupView()
|
||||
|
||||
accessCodeDialog?.dismiss()
|
||||
|
|
@ -232,6 +253,15 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
layoutButtonsAddCards.btnAddCard.isEnabled = false
|
||||
}
|
||||
|
||||
layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup)
|
||||
layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) }
|
||||
layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0
|
||||
|
||||
if (isSaltPay) {
|
||||
saltPayView.showAddBackupCards(state)
|
||||
return@with
|
||||
}
|
||||
|
||||
when (state.backupCardsNumber) {
|
||||
0 -> {
|
||||
cardsWidget.toFan {
|
||||
|
|
@ -258,10 +288,6 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
cardsWidget.getSecondBackupCardView().animate().alpha(1f).duration = 400
|
||||
}
|
||||
}
|
||||
|
||||
layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup)
|
||||
layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) }
|
||||
layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0
|
||||
}
|
||||
|
||||
private fun showSetAccessCode() {
|
||||
|
|
@ -307,7 +333,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
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) }
|
||||
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) },
|
||||
)
|
||||
layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_backup_origin)
|
||||
layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
|
||||
|
|
@ -350,15 +376,14 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
}
|
||||
|
||||
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
|
||||
tvHeader.text =
|
||||
getString(R.string.onboarding_title_backup_card_format, cardNumber)
|
||||
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])
|
||||
cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]),
|
||||
)
|
||||
layoutButtonsCommon.btnMainAction.text = getString(
|
||||
R.string.onboarding_button_backup_card_format,
|
||||
cardNumber
|
||||
cardNumber,
|
||||
)
|
||||
layoutButtonsCommon.btnMainAction.setOnClickListener {
|
||||
store.dispatch(BackupAction.WriteBackupCard(cardNumber))
|
||||
|
|
@ -398,4 +423,192 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
|
|||
override fun handleOnBackPressed() {
|
||||
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) {
|
||||
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 -> {
|
||||
walletFragment.cardsWidget.toFan {
|
||||
walletFragment.cardsWidget.getFirstBackupCardView().animate().alpha(0.6f).duration = 200
|
||||
}
|
||||
tvHeader.text = walletFragment.getText(R.string.onboarding_title_no_backup_cards)
|
||||
tvBody.text = walletFragment.getText(R.string.onboarding_subtitle_no_backup_cards)
|
||||
}
|
||||
1 -> {
|
||||
tvHeader.text = walletFragment.getText(R.string.onboarding_title_one_backup_card)
|
||||
tvBody.text = walletFragment.getText(R.string.onboarding_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.Kyc -> handleKyc()
|
||||
SaltPayRegistrationStep.KycStart -> handleKycStart(state)
|
||||
SaltPayRegistrationStep.KycWaiting -> handleKycWaiting()
|
||||
SaltPayRegistrationStep.Finished -> handleFinished()
|
||||
}
|
||||
progressButton?.changeState(state.mainButtonState)
|
||||
}
|
||||
|
||||
private fun handleNoGas() {
|
||||
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 handleKyc() = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ class ConfirmDiscardingBackupDialog {
|
|||
setTitle(R.string.welcome_interrupted_backup_discard_title)
|
||||
setMessage(R.string.welcome_interrupted_backup_discard_message)
|
||||
setPositiveButton(R.string.welcome_interrupted_backup_discard_resume) { _, _ ->
|
||||
store.dispatch(BackupAction.ResumeBackup)
|
||||
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup)
|
||||
}
|
||||
setNegativeButton(R.string.welcome_interrupted_backup_discard_discard) { _, _ ->
|
||||
store.dispatch(BackupAction.DiscardSavedBackup)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class UnfinishedBackupFoundDialog {
|
|||
setMessage(R.string.welcome_interrupted_backup_alert_message)
|
||||
setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ ->
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
store.dispatch(BackupAction.ResumeBackup)
|
||||
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup)
|
||||
}
|
||||
setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ ->
|
||||
store.dispatch(GlobalAction.HideDialog)
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.tap.features.onboarding.service
|
||||
|
||||
//class OnboardingWalletService(
|
||||
// override val fromScreen: AppScreen,
|
||||
// override var scanResponse: ScanResponse,
|
||||
// cardInfoStorage: UsedCardsPrefStorage,
|
||||
// walletManagerFactory: WalletManagerFactory,
|
||||
//) : ProductOnboardingService(cardInfoStorage, walletManagerFactory) {
|
||||
//
|
||||
// override fun navigateToScreen(): AppScreen {
|
||||
// return AppScreen.OnboardingWallet
|
||||
// }
|
||||
//}
|
||||
Loading…
Add table
Add a link
Reference in a new issue