Updated on 2026-08-14

This commit is contained in:
Tangem 2022-10-17 20:02:36 +05:00
parent d0c4d92a8c
commit 5fea60f2a2
83 changed files with 2801 additions and 775 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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