Updated on 2026-08-14

This commit is contained in:
Tangem 2023-02-27 16:55:53 +03:00
commit 2c78e27915
140 changed files with 3963 additions and 630 deletions

View file

@ -48,6 +48,8 @@ object DemoHelper {
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)
fun isTestDemoCard(scanResponse: ScanResponse): Boolean = config.isTestDemoCardId(scanResponse.card.cardId)
fun isDemoCardId(cardId: String): Boolean = config.isDemoCardId(cardId)
fun tryHandle(appState: () -> AppState?, action: Action): Boolean {
@ -108,6 +110,8 @@ class DemoConfig {
fun isDemoCardId(cardId: String): Boolean = demoCardIds.contains(cardId)
fun isTestDemoCardId(cardId: String): Boolean = testDemoCardIds.contains(cardId)
fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy()
?: Amount(BigDecimal.ZERO, blockchain).copy()
@ -118,9 +122,6 @@ class DemoConfig {
@Suppress("ClassOrdering")
private val releaseDemoCardIds = mutableListOf(
// === Not from the Google Sheet table ===
"FB10000000000196", // Note BTC
"FB20000000000186", // Note ETH
"FB30000000000176", // Wallet
"AC01000000041225",
"AC01000000041472",
"AB01000000046498",

View file

@ -24,8 +24,6 @@ sealed class DetailsAction : Action {
object Success : ResetToFactory()
}
object CreateBackup : DetailsAction()
object ScanCard : DetailsAction()
data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction()

View file

@ -66,12 +66,6 @@ class DetailsMiddleware {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
is DetailsAction.CreateBackup -> {
state.scanResponse?.let {
store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
}
DetailsAction.ScanCard -> {
scope.launch {
tangemSdkManager.scanProduct(userTokensRepository)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.ui.details
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
@ -86,7 +87,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
}
SettingsElement.LinkMoreCards -> {
Analytics.send(Settings.ButtonCreateBackup())
store.dispatch(DetailsAction.CreateBackup)
store.dispatch(WalletAction.MultiWallet.BackupWallet)
}
SettingsElement.TermsOfService -> {
store.dispatch(DisclaimerAction.Show(AppScreen.Details))

View file

@ -29,7 +29,7 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(IntroductionProcess.ScreenOpened())
store.dispatch(HomeAction.OnCreate)
store.dispatch(HomeAction.Init)
}

View file

@ -1,16 +1,18 @@
package com.tangem.tap.features.home.redux
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.entities.IndeterminateProgressButton
import org.rekotlin.Action
sealed class HomeAction : Action {
object OnCreate : HomeAction()
object Init : HomeAction()
data class ReadCard(
val analyticsEvent: AnalyticsEvent? = IntroductionProcess.CardWasScanned(),
val analyticsEvent: AnalyticsEvent? = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Introduction),
) : HomeAction()
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()

View file

@ -5,10 +5,12 @@ import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
@ -49,6 +51,10 @@ private val homeMiddleware: Middleware<AppState> = { _, _ ->
private fun handleHomeAction(action: Action) {
when (action) {
is HomeAction.OnCreate -> {
Analytics.eraseContext()
Analytics.send(IntroductionProcess.ScreenOpened())
}
is HomeAction.Init -> {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.ExchangeManager.Init)

View file

@ -1,9 +1,12 @@
package com.tangem.tap.features.onboarding
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.removeContext
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
@ -55,6 +58,7 @@ object OnboardingHelper {
accessCode: String? = null,
backupCardsIds: List<String>? = null,
) {
Analytics.setContext(scanResponse)
when {
// When should save user wallets, then save card without navigate to save wallet screen
preferencesStorage.shouldSaveUserWallets -> scope.launch {
@ -92,4 +96,8 @@ object OnboardingHelper {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
fun onInterrupted() {
Analytics.removeContext()
}
}

View file

@ -12,23 +12,39 @@ import com.tangem.tap.preferencesStorage
/**
[REDACTED_AUTHOR]
*/
object OnboardingSaltPayHelper {
suspend fun isOnboardingCase(scanResponse: ScanResponse, manager: SaltPayActivationManager): Result<Boolean> {
return try {
var updatedStep = manager.update(SaltPayActivationStep.None, null).successOr { return it }
class OnboardingSaltPayHelper {
companion object {
val cardStorage = preferencesStorage.usedCardsPrefStorage
val activationIsFinished = cardStorage.isActivationFinished(scanResponse.card.cardId)
if (updatedStep == SaltPayActivationStep.Success && activationIsFinished) {
updatedStep = SaltPayActivationStep.Finished
suspend fun isOnboardingCase(
scanResponse: ScanResponse,
manager: SaltPayActivationManager,
): Result<Boolean> {
return try {
val updatedStep = manager.update(SaltPayActivationStep.None, null).successOr { return it }
val cardStorage = preferencesStorage.usedCardsPrefStorage
val activationIsFinished = cardStorage.isActivationFinished(scanResponse.card.cardId)
if (updatedStep == SaltPayActivationStep.Success && !activationIsFinished) {
cardStorage.activationFinished(scanResponse.card.cardId)
}
val isActivationCase = updatedStep != SaltPayActivationStep.Success
val isBackupCase = scanResponse.card.backupStatus?.isActive == false
Result.Success(isActivationCase || isBackupCase)
} catch (ex: Exception) {
Result.Failure(ex)
} catch (error: SaltPayActivationError) {
Result.Failure(error)
}
val isActivationCase = updatedStep != SaltPayActivationStep.Finished
val isBackupCase = scanResponse.card.backupStatus?.isActive == false
Result.Success(isActivationCase || isBackupCase)
} catch (ex: Exception) {
Result.Failure(ex)
} catch (error: SaltPayActivationError) {
Result.Failure(error)
}
fun testProceedToOnboarding(
scanResponse: ScanResponse,
manager: SaltPayActivationManager,
): Result<Boolean> = Result.Success(true)
fun testProceedToMainScreen(
scanResponse: ScanResponse,
manager: SaltPayActivationManager,
): Result<Boolean> = Result.Success(false)
}
}

View file

@ -196,6 +196,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
store.dispatchDialogShow(
OnboardingDialog.InterruptOnboarding(
onOk = {
OnboardingHelper.onInterrupted()
store.dispatch(NavigationAction.PopBackTo())
},
),

View file

@ -118,4 +118,9 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
}
override fun handleOnBackPressed() {
store.dispatch(OnboardingOtherCardsAction.OnBackPressed)
super.handleOnBackPressed()
}
}

View file

@ -15,6 +15,7 @@ sealed class OnboardingOtherCardsAction : Action {
class SetArtworkUrl(val artworkUrl: String) : OnboardingOtherCardsAction()
object DetermineStepOfScreen : OnboardingOtherCardsAction()
object Done : OnboardingOtherCardsAction()
object OnBackPressed : OnboardingOtherCardsAction()
data class SetStepOfScreen(val step: OnboardingOtherCardsStep) : OnboardingOtherCardsAction()

View file

@ -119,9 +119,7 @@ private fun handleOtherCardsAction(action: Action) {
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))
}
is CompletionResult.Failure -> {
// do nothing
}
is CompletionResult.Failure -> Unit
}
}
}
@ -130,9 +128,9 @@ private fun handleOtherCardsAction(action: Action) {
store.dispatch(GlobalAction.Onboarding.Stop)
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(onboardingManager.scanResponse)
}
is OnboardingOtherCardsAction.Confetti.Hide,
is OnboardingOtherCardsAction.SetArtworkUrl,
is OnboardingOtherCardsAction.Confetti.Show,
-> Unit
OnboardingOtherCardsAction.OnBackPressed -> {
OnboardingHelper.onInterrupted()
}
else -> Unit
}
}

View file

@ -3,8 +3,8 @@ package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.Message
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.ScanResponse
import com.tangem.datasource.utils.AssetReader
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance

View file

@ -320,6 +320,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
twinCardsState.currentStep != TwinCardsStep.TopUpWallet &&
twinCardsState.currentStep != TwinCardsStep.Done
OnboardingHelper.onInterrupted()
store.dispatch(TwinCardsAction.CardsManager.Release)
action.shouldResetTwinCardsWidget(shouldReturnCardBack) {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))

View file

@ -411,7 +411,11 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
TwinCardsAction.OnBackPressed { should, popAction ->
store.dispatch(TwinCardsAction.Confetti.Hide)
showConfetti(false)
if (should) switchToCard(TwinCardNumber.First, true, popAction) else popAction()
if (should) {
switchToCard(TwinCardNumber.First, true, popAction)
} else {
popAction()
}
},
)
}

View file

@ -31,9 +31,8 @@ sealed class BackupAction : Action {
object ScanPrimaryCard : BackupAction()
/**
* Check for unfinished backup of standard Wallet cards.
* For SaltPay cards unfinished backup resumed after scanning the card on HomeScreen through Onboarding.Start.
* See more Onboarding.Start, CheckForUnfinishedBackup, StartForUnfinishedBackup
* Check for unfinished backup of standard Wallets and SaltPay cards
* See more GlobalAction.Onboarding.StartForUnfinishedBackup
*/
object CheckForUnfinishedBackup : BackupAction()
@ -65,7 +64,11 @@ sealed class BackupAction : Action {
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
object FinishBackup : BackupAction()
/**
* It always calls for the SaltPay cards when resuming the activation process.
*/
data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction()
object DiscardBackup : BackupAction()
object DiscardSavedBackup : BackupAction()
object ResumeFoundUnfinishedBackup : BackupAction()

View file

@ -24,8 +24,8 @@ import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory
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
@ -72,7 +72,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
}
when {
card == null -> {
// it's possible when found unfinished backup for standard Wallet cards
// it's possible when found unfinished backup
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> {
@ -87,10 +87,11 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
scanResponse.cardTypesResolver.isSaltPay() -> {
store.dispatch(OnboardingWalletAction.GetToSaltPayStep)
store.dispatch(BackupAction.FinishBackup)
store.dispatch(BackupAction.FinishBackup(withAnalytics = false))
store.dispatch(OnboardingSaltPayAction.OnSwitchedToSaltPayProcess)
}
else -> store.dispatch(BackupAction.FinishBackup)
else -> store.dispatch(BackupAction.FinishBackup())
}
}
else -> {
@ -317,7 +318,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
is CompletionResult.Success -> {
if (backupService.currentState == BackupService.State.Finished) {
initSaltPayOnBackupFinishedIfNeeded(scanResponse, onboardingWalletState)
store.dispatchOnMain(BackupAction.FinishBackup)
store.dispatchOnMain(BackupAction.FinishBackup())
} else {
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1))
}
@ -328,7 +329,9 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
is BackupAction.FinishBackup -> {
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
if (action.withAnalytics) {
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
}
if (!onboardingWalletState.isSaltPay) {
Analytics.send(Onboarding.Finished())
finishCardActivation(backupState, card)
@ -345,18 +348,23 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
backupService.discardSavedBackup()
}
is BackupAction.CheckForUnfinishedBackup -> {
if (backupService.hasIncompletedBackup && !backupService.primaryCardIsSaltPayVisa()) {
if (backupService.hasIncompletedBackup) {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound))
}
}
is BackupAction.ResumeFoundUnfinishedBackup -> {
store.dispatch(GlobalAction.Onboarding.StartForUnfinishedBackup(backupService.addedBackupCardsCount))
store.dispatch(
GlobalAction.Onboarding.StartForUnfinishedBackup(
addedBackupCardsCount = backupService.addedBackupCardsCount,
isSaltPayVisa = backupService.primaryCardIsSaltPayVisa(),
),
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
is BackupAction.DismissBackup -> {
Analytics.send(Onboarding.Backup.Skipped())
if (onboardingWalletState.isSaltPay) throw UnsupportedOperationException()
store.dispatch(BackupAction.FinishBackup)
store.dispatch(BackupAction.FinishBackup())
}
else -> {}
}
@ -395,14 +403,18 @@ private fun finishCardsActivationForDiscardedUnfinishedBackup(cardId: String) {
*/
private fun initSaltPayOnBackupFinishedIfNeeded(
scanResponse: ScanResponse?,
onboardingWalletState: OnboardingWalletState,
state: OnboardingWalletState,
) {
if (onboardingWalletState.isSaltPay && onboardingWalletState.onboardingSaltPayState == null) {
if (scanResponse == null) error("scanning response is null")
if (state.backupState.isInterruptedBackup) return
if (scanResponse == null) return
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager, config))
store.dispatchOnMain(OnboardingSaltPayAction.Update)
if (state.isSaltPay && state.onboardingSaltPayState == null) {
val manager = SaltPayActivationManagerFactory(
blockchain = scanResponse.cardTypesResolver.getBlockchain(),
card = scanResponse.card,
).create()
store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager))
store.dispatchOnMain(OnboardingSaltPayAction.Update(false))
}
}
@ -420,6 +432,7 @@ private fun handleOnBackPressed(state: OnboardingWalletState) {
if (state.isSaltPay) {
showInterruptOnboardingDialog()
} else {
OnboardingHelper.onInterrupted()
store.dispatch(NavigationAction.PopBackTo())
}
}
@ -430,6 +443,7 @@ private fun showInterruptOnboardingDialog() {
store.dispatchDialogShow(
OnboardingDialog.InterruptOnboarding(
onOk = {
OnboardingHelper.onInterrupted()
store.dispatch(BackupAction.DiscardBackup)
store.dispatch(NavigationAction.PopBackTo())
},

View file

@ -38,13 +38,13 @@ private object ReducerForGlobalAction {
fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState {
return when (action) {
is GlobalAction.Onboarding.Start -> {
val isSaltPay = action.scanResponse.cardTypesResolver.isSaltPay()
OnboardingWalletState(
backupState = state.backupState.copy(
maxBackupCards = if (action.scanResponse.cardTypesResolver.isSaltPay()) 1 else 2,
canSkipBackup =
if (action.scanResponse.cardTypesResolver.isSaltPay()) false else action.canSkipBackup,
maxBackupCards = maxBackupCards(isSaltPay),
canSkipBackup = if (isSaltPay) false else action.canSkipBackup,
),
isSaltPay = action.scanResponse.cardTypesResolver.isSaltPay(),
isSaltPay = isSaltPay,
)
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
@ -52,13 +52,16 @@ private object ReducerForGlobalAction {
backupState = state.backupState.copy(
maxBackupCards = action.addedBackupCardsCount,
canSkipBackup = false,
isInterruptedBackup = true,
),
isSaltPay = false,
isSaltPay = action.isSaltPayVisa,
)
}
else -> state
}
}
private fun maxBackupCards(isSaltPay: Boolean): Int = if (isSaltPay) 1 else 2
}
private object BackupReducer {

View file

@ -62,7 +62,7 @@ data class OnboardingWalletState(
-> 9
SaltPayActivationStep.Claim -> 10
SaltPayActivationStep.ClaimInProgress -> 11
SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success, SaltPayActivationStep.Finished,
SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success,
-> getMaxProgress()
}
}
@ -87,6 +87,7 @@ data class BackupState(
val backupStep: BackupStep = BackupStep.InitBackup,
val maxBackupCards: Int = 2,
val canSkipBackup: Boolean = true,
val isInterruptedBackup: Boolean = false,
)
enum class AccessCodeError {

View file

@ -23,6 +23,8 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
import java.math.BigInteger
import java.math.MathContext
import java.math.RoundingMode
import java.util.concurrent.atomic.AtomicLong
/**
@ -33,11 +35,12 @@ class GnosisRegistrator(
) {
private val walletAddress = walletManager.wallet.address
private val blockchain = walletManager.wallet.blockchain
private val token = walletManager.wallet.getFirstToken().guard {
throw NullPointerException("WalletManager for GnosisRegistrator must contain token")
}
private val otpProcessorContractAddress: String = when (walletManager.wallet.blockchain) {
private val otpProcessorContractAddress: String = when (blockchain) {
Blockchain.SaltPay -> "0xc659f4FEd7A84a188F54cBA4A7a49D77c1a20522"
else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain")
}
@ -47,6 +50,7 @@ class GnosisRegistrator(
private val atomicNonce = AtomicLong()
suspend fun checkHasGas(): Result<Boolean> {
// return Result.Success(true) // test
val wallet = walletManager.safeUpdate().successOr {
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
}
@ -145,6 +149,7 @@ class GnosisRegistrator(
}
suspend fun getAllowance(): Result<Amount> {
// return Result.Success(Amount(BigDecimal(0.1), walletManager.wallet.blockchain)) // test
return walletManager.getAllowance(addressTreasureSafe, token)
}
@ -154,6 +159,12 @@ class GnosisRegistrator(
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
}
}
// return transferFromStandardWay(amountToClaim, signer)
return transferFromHardcodeWay(amountToClaim, signer)
}
private suspend fun transferFromStandardWay(amountToClaim: BigDecimal, signer: TransactionSigner): Result<Unit> {
val amount = Amount(token, amountToClaim)
val feeAmount = walletManager.getFeeToTransferFrom(amount, addressTreasureSafe)
.extractFeeAmount().successOr { return it }
@ -164,6 +175,34 @@ class GnosisRegistrator(
return Result.Success(Unit)
}
private suspend fun transferFromHardcodeWay(amountToClaim: BigDecimal, signer: TransactionSigner): Result<Unit> {
val amount = Amount(token, amountToClaim)
val gasPrice = walletManager.getGasPrice().successOr { return it }
val hardcodeGasLimit = BigInteger("300000")
val hardcodeFeeValue = (hardcodeGasLimit * gasPrice).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
).movePointLeft(blockchain.decimals())
val hardcodeFeeAmount = Amount(hardcodeFeeValue, blockchain)
val transactionData = walletManager.createTransferFromTransaction(
amount = amount,
fee = hardcodeFeeAmount,
source = addressTreasureSafe,
)
val transactionToSign = walletManager.transactionBuilder.buildTransferFromToSign(
transactionData,
walletManager.txCount.toBigInteger(),
hardcodeGasLimit,
) ?: return Result.Failure(BlockchainSdkError.CustomError("Not enough data"))
return when (val result = walletManager.signAndSend(transactionToSign, signer)) {
SimpleResult.Success -> Result.Success(Unit)
is SimpleResult.Failure -> Result.Failure(result.error)
}
}
@Suppress("MagicNumber")
private fun Result<List<Amount>>.extractFeeAmount(): Result<Amount> {
return when (this) {

View file

@ -1,14 +1,20 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import android.net.Uri
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.Token
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.extensions.successOr
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.isZero
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
@ -23,6 +29,8 @@ import com.tangem.datasource.api.paymentology.models.response.RegistrationRespon
import com.tangem.datasource.api.paymentology.models.response.tryExtractError
import com.tangem.datasource.config.models.KYCProvider
import com.tangem.datasource.config.models.SaltPayConfig
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.extensions.successOr
import com.tangem.operations.attestation.AttestWalletKeyResponse
import com.tangem.tap.common.extensions.safeUpdate
@ -31,6 +39,7 @@ import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import com.tangem.tap.store
import java.math.BigDecimal
import com.tangem.blockchain.extensions.Result as BlockchainResult
@ -227,4 +236,96 @@ class KYCUrlProvider(
.appendQueryParameter(kycProvider.externalIdParameterKey, kycRefId)
.build().toString()
}
}
class SaltPayActivationManagerFactory(
private val blockchain: Blockchain,
private val card: CardDTO,
) {
fun create(): SaltPayActivationManager {
val wallet = card.wallets.first()
val saltPayConfig = store.state.globalState.configManager?.config?.saltPayConfig.guard {
throw NullPointerException("SaltPayConfig is not initialized")
}
val wmFactory = store.state.globalState.tapWalletManager.walletManagerFactory
val paymentologyService = store.state.domainNetworks.paymentologyService
// return createDummyActivationManager(saltPayConfig, wmFactory, paymentologyService)
return createActivationPayManager(
cardId = card.cardId,
cardPublicKey = card.cardPublicKey,
kycProvider = saltPayConfig.kycProvider,
gnosisRegistrator = GnosisRegistrator(
walletManager = makeSaltPayWalletManager(
blockchain = blockchain,
walletPublicKey = wallet.publicKey,
wmFactory = wmFactory,
),
),
paymentologyService = paymentologyService,
)
}
private fun makeSaltPayWalletManager(
blockchain: Blockchain,
walletPublicKey: ByteArray,
wmFactory: WalletManagerFactory,
): EthereumWalletManager {
return wmFactory.makeWalletManager(
blockchain = blockchain,
publicKey = Wallet.PublicKey(walletPublicKey, null, null),
tokens = listOf(SaltPayWorkaround.tokenFrom(blockchain)),
) as EthereumWalletManager
}
private fun createActivationPayManager(
cardId: String,
cardPublicKey: ByteArray,
kycProvider: KYCProvider,
gnosisRegistrator: GnosisRegistrator,
paymentologyService: PaymentologyApiService,
): SaltPayActivationManager {
return SaltPayActivationManager(
cardId = cardId,
cardPublicKey = cardPublicKey,
kycProvider = kycProvider,
paymentologyService = paymentologyService,
gnosisRegistrator = gnosisRegistrator,
)
}
private fun createDummyActivationManager(
saltPayConfig: SaltPayConfig,
wmFactory: WalletManagerFactory,
paymentologyService: PaymentologyApiService,
): SaltPayActivationManager {
val fakeSource = DummySaltPayCardSource()
val walletManager = wmFactory.makeWalletManager(
blockchain = fakeSource.blockchain,
publicKey = fakeSource.blockchainPublicKey,
tokens = listOf(fakeSource.token),
) as EthereumWalletManager
return createActivationPayManager(
cardId = fakeSource.cardId,
cardPublicKey = fakeSource.cardPublicKey,
kycProvider = saltPayConfig.kycProvider,
gnosisRegistrator = GnosisRegistrator(walletManager),
paymentologyService = paymentologyService,
)
}
private data class DummySaltPayCardSource(
val blockchain: Blockchain = Blockchain.SaltPay,
val cardId: String = "FF03000001001057",
private val cardPublicKeyString: String = "039C2D2F9B68003766BFC29766761F55F4084E48B8B012BF439E115A79AB122D48",
private val walletPublicKeyString: String = "0399B2DF5B129FBF69097DAA7C44FB5849E578CCFC22228A409870C74B56C4C3D1",
) {
val cardPublicKey: ByteArray = cardPublicKeyString.hexToBytes()
val walletPublicKey: ByteArray = walletPublicKeyString.hexToBytes()
val blockchainPublicKey = Wallet.PublicKey(walletPublicKey, null, null)
val token: Token = SaltPayWorkaround.tokenFrom(blockchain)
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
@ -12,12 +14,20 @@ import com.tangem.tap.store
[REDACTED_AUTHOR]
*/
object SaltPayExceptionHandler {
fun handle(throwable: Throwable) {
when (throwable) {
is SaltPayActivationError -> {
val dialog = when (throwable) {
is SaltPayActivationError.NoGas -> SaltPayDialog.Activation.NoGas
is SaltPayActivationError.NoGas -> {
Analytics.send(Onboarding.NotEnoughGasError())
SaltPayDialog.Activation.NoGas
}
is SaltPayActivationError.PutVisaCard -> SaltPayDialog.Activation.PutVisaCard
is SaltPayActivationError.CardNotPassed -> {
Analytics.send(Onboarding.CardNotPassedError())
SaltPayDialog.Activation.OnError(throwable)
}
else -> SaltPayDialog.Activation.OnError(throwable)
}
store.dispatchDialogShow(dialog)
@ -25,7 +35,7 @@ object SaltPayExceptionHandler {
is TangemSdkError -> {
when (throwable) {
is TangemSdkError.NetworkError -> {
val message = throwable.customMessage
val message = (throwable).customMessage
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message))
}
else -> Unit

View file

@ -14,16 +14,20 @@ import com.tangem.wallet.R
[REDACTED_AUTHOR]
*/
object NoFundsForActivationDialog {
fun create(context: Context): Dialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.saltpay_error_no_gas_title)
setMessage(R.string.saltpay_error_no_gas_message)
setPositiveButton(R.string.chat_button_title) { _, _ ->
val config = store.state.globalState.configManager?.config?.saltPayConfig?.zendesk.guard {
store.dispatchDebugErrorNotification("SaltPayConfig not initialized")
return@setPositiveButton
}
store.dispatch(GlobalAction.OpenChat(SupportInfo(), config))
val sprinklrConfig = store.state.globalState.configManager?.config
?.saltPayConfig
?.sprinklr
.guard {
store.dispatchDebugErrorNotification("SaltPayConfig not initialized")
return@setPositiveButton
}
store.dispatch(GlobalAction.OpenChat(SupportInfo(), sprinklrConfig))
}
setNegativeButton(R.string.common_cancel) { _, _ ->
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
import com.tangem.blockchain.common.Amount
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
import com.tangem.datasource.config.models.SaltPayConfig
import org.rekotlin.Action
import java.math.BigDecimal
@ -12,9 +11,13 @@ import java.math.BigDecimal
sealed class OnboardingSaltPayAction : Action {
object Init : OnboardingSaltPayAction()
/**
* Notification about the switching from the Wallet view to the SaltPay view.
*/
object OnSwitchedToSaltPayProcess : OnboardingSaltPayAction()
data class SetDependencies(
val registrationManager: SaltPayActivationManager,
val saltPayConfig: SaltPayConfig,
) : OnboardingSaltPayAction()
data class SetInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction()
@ -22,7 +25,7 @@ sealed class OnboardingSaltPayAction : Action {
data class SetAccessCode(val accessCode: String?) : OnboardingSaltPayAction()
object Update : OnboardingSaltPayAction()
data class Update(val withAnalytics: Boolean = true) : OnboardingSaltPayAction()
object RegisterCard : OnboardingSaltPayAction()
object OpenUtorgKYC : OnboardingSaltPayAction()
object UtorgKYCRedirectSuccess : OnboardingSaltPayAction()
@ -33,7 +36,11 @@ sealed class OnboardingSaltPayAction : Action {
data class TrySetPin(val pin: String) : OnboardingSaltPayAction()
data class SetPin(val pin: String) : OnboardingSaltPayAction()
data class SetStep(val newStep: SaltPayActivationStep) : OnboardingSaltPayAction()
data class SetStep(
val newStep: SaltPayActivationStep,
val withAnalytics: Boolean = true,
) : OnboardingSaltPayAction()
data class SetAmountToClaim(val amount: Amount?) : OnboardingSaltPayAction()
data class SetTokenBalance(val balanceValue: BigDecimal) : OnboardingSaltPayAction()
}

View file

@ -78,6 +78,9 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
onboardingManager.activationStarted(card.cardId)
}
}
is OnboardingSaltPayAction.OnSwitchedToSaltPayProcess -> {
sendAnalyticsScreenOpened(getState().step, newStep = SaltPayActivationStep.None)
}
is OnboardingSaltPayAction.Update -> {
handleInProgress = true
@ -89,7 +92,18 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
}
handleInProgress = false
dispatchOnMain(OnboardingSaltPayAction.SetStep(updateStep))
dispatchOnMain(OnboardingSaltPayAction.SetStep(updateStep, action.withAnalytics))
if (updateStep == SaltPayActivationStep.Claim) {
handleClaimRefreshInProgress = true
val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr {
SaltPayExceptionHandler.handle(it.error)
handleClaimRefreshInProgress = false
return@launch
}
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue))
handleClaimRefreshInProgress = false
}
}
}
is OnboardingSaltPayAction.RegisterCard -> {
@ -136,8 +150,16 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
return@launch
}
Analytics.send(Onboarding.PinCodeSet())
var nextStep = SaltPayActivationStep.KycIntro
nextStep = saltPayManager.update(nextStep, state.amountToClaim).successOr {
onException(it.error)
return@launch
}
handleInProgress = false
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycIntro))
dispatchOnMain(OnboardingSaltPayAction.SetStep(nextStep))
}
}
is OnboardingSaltPayAction.OpenUtorgKYC -> {
@ -159,13 +181,13 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
}
handleInProgress = false
dispatchOnMain(OnboardingSaltPayAction.Update)
dispatchOnMain(OnboardingSaltPayAction.Update())
}
}
is OnboardingSaltPayAction.TrySetPin -> {
try {
assertPinValid(action.pin, getState().pinLength)
Analytics.send(Onboarding.PinCodeSet())
Analytics.send(Onboarding.ButtonSetPinCode())
store.dispatch(OnboardingSaltPayAction.SetPin(action.pin))
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.CardRegistration))
} catch (error: SaltPayActivationError) {
@ -219,54 +241,48 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
val state = getState()
scope.launch {
when (val amountToClaimResult = state.saltPayManager.getAmountToClaim()) {
is Result.Success -> {
// need to re claim
handleClaimRefreshInProgress = false
dispatchOnMain(OnboardingSaltPayAction.SetAmountToClaim(amountToClaimResult.data))
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Claim))
}
is Result.Failure -> {
when (amountToClaimResult.error) {
is SaltPayActivationError.NoFundsToClaim -> {
val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr {
SaltPayExceptionHandler.handle(it.error)
handleClaimRefreshInProgress = false
return@launch
}
val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr {
SaltPayExceptionHandler.handle(it.error)
handleClaimRefreshInProgress = false
return@launch
}
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue))
handleClaimRefreshInProgress = false
if (tokenAmountValue.isPositive()) {
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue))
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess))
}
}
else -> {
handleClaimRefreshInProgress = false
SaltPayExceptionHandler.handle(amountToClaimResult.error)
}
}
val amountToClaim = state.saltPayManager.getAmountToClaim().successOr {
if (it.error !is SaltPayActivationError.NoFundsToClaim) {
handleClaimRefreshInProgress = false
SaltPayExceptionHandler.handle(it.error)
return@launch
} else {
null
}
}
handleClaimRefreshInProgress = false
if (tokenAmountValue.isPositive() && amountToClaim == null) {
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess))
} else {
// dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimSuccess))
}
}
}
is OnboardingSaltPayAction.SetStep -> {
when (action.newStep) {
SaltPayActivationStep.KycStart -> Analytics.send(Onboarding.KYCStarted())
SaltPayActivationStep.KycWaiting -> Analytics.send(Onboarding.KYCInProgress())
SaltPayActivationStep.KycReject -> Analytics.send(Onboarding.KYCRejected())
SaltPayActivationStep.Claim -> Analytics.send(Onboarding.ClaimScreenOpened())
SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success -> {
Analytics.send(Onboarding.Finished())
val onboardingManager = getOnboardingManager().guard {
// Null is possible if it is started from a standard pending backup and in this case
// it is impossible to get here
store.dispatchDebugErrorNotification("OnboardingManager can't be NULL")
return
}
finishCardActivation(getOnboardingWalletState().backupState, onboardingManager.scanResponse.card)
if (action.withAnalytics) {
sendAnalyticsScreenOpened(getState().step, action.newStep)
}
if (action.newStep == SaltPayActivationStep.ClaimSuccess ||
action.newStep == SaltPayActivationStep.Success
) {
val onboardingManager = getOnboardingManager().guard {
// Null is possible if it is started from a standard pending backup and in this case
// it is impossible to get here
store.dispatchDebugErrorNotification("OnboardingManager can't be NULL")
return
}
else -> {}
finishCardActivation(
getOnboardingWalletState().backupState,
onboardingManager.scanResponse.card,
)
}
}
else -> {
@ -275,6 +291,22 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
}
}
private fun sendAnalyticsScreenOpened(currentStep: SaltPayActivationStep, newStep: SaltPayActivationStep) {
if (currentStep == newStep) return
when (newStep) {
SaltPayActivationStep.NeedPin -> Analytics.send(Onboarding.PinScreenOpened())
SaltPayActivationStep.CardRegistration -> Analytics.send(Onboarding.CardConnectionScreenOpened())
SaltPayActivationStep.KycIntro -> Analytics.send(Onboarding.KYCScreenOpened())
SaltPayActivationStep.KycStart -> Analytics.send(Onboarding.KYCStarted())
SaltPayActivationStep.KycWaiting -> Analytics.send(Onboarding.KYCInProgress())
SaltPayActivationStep.KycReject -> Analytics.send(Onboarding.KYCRejected())
SaltPayActivationStep.Claim -> Analytics.send(Onboarding.ClaimScreenOpened())
SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success -> Analytics.send(Onboarding.Finished())
else -> {}
}
}
suspend fun SaltPayActivationManager.update(
currentStep: SaltPayActivationStep,
currentAmountToClaim: Amount?,
@ -362,6 +394,7 @@ private suspend fun getAmountToClaimIfNeeded(
return amountToClaim
}
// return Amount(BigDecimal(0.1), Blockchain.SaltPay)
return when (val result = saltPayManager.getAmountToClaim()) {
is Result.Success -> {
Timber.d("getAmountToClaimIfNeeded: success: %s", result.data)

View file

@ -20,7 +20,6 @@ private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingW
return onboardingWalletState.copy(
onboardingSaltPayState = OnboardingSaltPayState(
saltPayManager = action.registrationManager,
saltPayConfig = action.saltPayConfig,
),
)
}

View file

@ -2,19 +2,10 @@ package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.extensions.guard
import com.tangem.datasource.api.paymentology.PaymentologyApiService
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.ScanResponse
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.datasource.config.models.KYCProvider
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
import com.tangem.datasource.config.models.SaltPayConfig
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.store
import java.math.BigDecimal
/**
@ -23,7 +14,6 @@ import java.math.BigDecimal
data class OnboardingSaltPayState(
@Transient
val saltPayManager: SaltPayActivationManager = SaltPayActivationManager.stub(),
val saltPayConfig: SaltPayConfig = SaltPayConfig.stub(),
val pinCode: String? = null,
val accessCode: String? = null,
val amountToClaim: Amount? = null,
@ -38,54 +28,6 @@ data class OnboardingSaltPayState(
get() = if (inProgress) ProgressState.Loading else ProgressState.Done
val pinLength: Int = 4
companion object {
fun initDependency(scanResponse: ScanResponse): Pair<SaltPayActivationManager, SaltPayConfig> {
val globalState = store.state.globalState
val saltPayConfig = globalState.configManager?.config?.saltPayConfig.guard {
throw NullPointerException("SaltPayConfig is not initialized")
}
val gnosisRegistrator = makeGnosisRegistrator(
scanResponse = scanResponse,
wmFactory = globalState.tapWalletManager.walletManagerFactory,
)
val registrationManager = makeSaltPayRegistrationManager(
scanResponse = scanResponse,
gnosisRegistrator = gnosisRegistrator,
paymentologyService = store.state.domainNetworks.paymentologyService,
kycProvider = saltPayConfig.kycProvider,
)
return registrationManager to saltPayConfig
}
fun makeSaltPayRegistrationManager(
scanResponse: ScanResponse,
gnosisRegistrator: GnosisRegistrator,
paymentologyService: PaymentologyApiService,
kycProvider: KYCProvider,
): SaltPayActivationManager {
if (!scanResponse.cardTypesResolver.isSaltPay()) {
throw IllegalArgumentException("Can't initialize the OnboardingSaltPayMiddleware if card is not SalPay")
}
return SaltPayActivationManager(
cardId = scanResponse.card.cardId,
cardPublicKey = scanResponse.card.cardPublicKey,
kycProvider = kycProvider,
paymentologyService = paymentologyService,
gnosisRegistrator = gnosisRegistrator,
)
}
fun makeGnosisRegistrator(
scanResponse: ScanResponse,
wmFactory: WalletManagerFactory,
): GnosisRegistrator {
return GnosisRegistrator(
walletManager = wmFactory.makeSaltPayWalletManager(scanResponse),
)
}
}
}
enum class SaltPayActivationStep {
@ -101,5 +43,4 @@ enum class SaltPayActivationStep {
ClaimInProgress,
ClaimSuccess,
Success,
Finished;
}

View file

@ -80,9 +80,10 @@ internal class OnboardingSaltPayView(
when (state.step) {
OnboardingWalletStep.SaltPay -> setSaltPayStep(state.onboardingSaltPayState)
OnboardingWalletStep.Backup -> {
if (state.backupState.backupStep == BackupStep.Finished) {
if (state.backupState.backupStep == BackupStep.Finished && !state.backupState.isInterruptedBackup) {
// if backup finished -> switch OnboardingWalletStep to the SaltPay
store.dispatch(OnboardingWalletAction.GetToSaltPayStep)
store.dispatch(OnboardingSaltPayAction.OnSwitchedToSaltPayProcess)
} else {
// if not -> back to the standard backup process
walletFragment.handleOnboardingStep(state)
@ -103,7 +104,7 @@ internal class OnboardingSaltPayView(
val uri = Uri.parse(state.onboardingSaltPayState.saltPayCardArtworkUrl)
walletFragment.loadImageIntoImageView(uri, imvFrontCard)
}
imvFirstBackupCard.load(R.drawable.img_salt_pay_visa)
imvFirstBackupCard.load(R.drawable.card_placeholder_wallet)
// if (state.onboardingSaltPayState?.saltPayCardArtworkUrl == null) {
// //if saltPay url not loaded -> load from resource
@ -233,7 +234,7 @@ internal class OnboardingSaltPayView(
btnKycAction.text = getText(R.string.onboarding_button_kyc_waiting)
btnKycAction.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.Update)
store.dispatch(OnboardingSaltPayAction.Update())
}
progressButton = SaltPayProgressButton(root)
}
@ -249,7 +250,7 @@ internal class OnboardingSaltPayView(
btnOpenSupportChat.hide()
btnKycAction.text = getText(R.string.onboarding_button_kyc_start)
btnKycAction.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.Update)
store.dispatch(OnboardingSaltPayAction.Update())
}
progressButton = SaltPayProgressButton(root)
}

View file

@ -40,8 +40,8 @@ internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment<SaveWa
@Composable
override fun ScreenContent(
state: SaveWalletScreenState,
modifier: Modifier,
state: SaveWalletScreenState,
) {
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())

View file

@ -0,0 +1,10 @@
package com.tangem.tap.features.sprinklr.redux
import com.tangem.datasource.config.models.SprinklrConfig
import org.rekotlin.Action
sealed interface SprinklrAction : Action {
data class Init(val userId: String, val config: SprinklrConfig) : SprinklrAction
data class UpdateUrl(val url: String) : SprinklrAction
data class UpdateSprinklrDomains(val domains: List<String>) : SprinklrAction
}

View file

@ -0,0 +1,42 @@
package com.tangem.tap.features.sprinklr.redux
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.sprinklr.redux.model.SprinklrUrl
import com.tangem.tap.store
import org.rekotlin.Middleware
internal class SprinklrMiddleware {
val middleware: Middleware<AppState> = { _, appStateProvider ->
{ next ->
{ action ->
val appState = appStateProvider()
if (action is SprinklrAction && appState != null) {
handleAction(action)
}
next(action)
}
}
}
private fun handleAction(action: SprinklrAction) {
when (action) {
is SprinklrAction.Init -> updateUrl(action.userId, action.config)
is SprinklrAction.UpdateUrl,
is SprinklrAction.UpdateSprinklrDomains,
-> Unit
}
}
private fun updateUrl(userId: String, config: SprinklrConfig) {
updateSprinklrDomains(config.baseUrl)
val url = SprinklrUrl.Prod(userId, config.baseUrl, config.appId).url
store.dispatchOnMain(SprinklrAction.UpdateUrl(url))
}
private fun updateSprinklrDomains(baseUrl: String) {
val domains = listOf(baseUrl, SprinklrUrl.Static.url)
store.dispatchOnMain(SprinklrAction.UpdateSprinklrDomains(domains))
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.features.sprinklr.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
internal object SprinklrReducer {
fun reduce(action: Action, state: AppState): SprinklrState {
return if (action is SprinklrAction) {
internalReduce(action, state.sprinklrState)
} else state.sprinklrState
}
private fun internalReduce(action: SprinklrAction, state: SprinklrState): SprinklrState {
return when (action) {
is SprinklrAction.Init -> state
is SprinklrAction.UpdateUrl -> state.copy(url = action.url)
is SprinklrAction.UpdateSprinklrDomains -> state.copy(sprinklrDomains = action.domains)
}
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.tap.features.sprinklr.redux
data class SprinklrState(
val url: String = "",
val sprinklrDomains: List<String> = emptyList(),
)

View file

@ -0,0 +1,27 @@
package com.tangem.tap.features.sprinklr.redux.model
internal sealed class SprinklrUrl {
abstract val url: String
class Prod(userId: String, baseUrl: String, appId: String) : SprinklrUrl() {
private val locale = java.util.Locale.getDefault().language
override val url: String = "$baseUrl/page" +
"?appId=$appId" +
"&device=$Device" +
"&enableClose=$CloseEnabled" +
"&zoom=$ZoomEnabled" +
"&locale=$locale" +
"&user_id=$userId"
companion object {
private const val Device = "MOBILE"
private const val CloseEnabled = true
private const val ZoomEnabled = false
}
}
object Static : SprinklrUrl() {
override val url: String = "live-chat-static.sprinklr.com"
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.tap.features.sprinklr.ui
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.ComposeActivity
import com.tangem.tap.common.analytics.events.Chat
internal class SprinklrActivity : ComposeActivity<SprinklrScreenState>() {
private val viewModel by viewModels<SprinklrViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(Chat.ScreenOpened())
viewModel.setNavigateBackCallback {
this.finish()
}
}
@Composable
override fun provideState(): State<SprinklrScreenState> {
return viewModel.state.collectAsState()
}
@Composable
override fun ScreenContent(modifier: Modifier, state: SprinklrScreenState) {
BackHandler(onBack = state.onNavigateBack)
SprinklrScreenContent(
modifier = modifier
.systemBarsPadding()
.imePadding(),
state = state,
)
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.tap.features.sprinklr.ui
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.tangem.tap.features.sprinklr.ui.webview.SprinklrWebViewClient
@SuppressLint("SetJavaScriptEnabled")
@Composable
internal fun SprinklrScreenContent(
state: SprinklrScreenState,
modifier: Modifier = Modifier,
) {
WebView(
modifier = modifier,
state = rememberWebViewState(url = state.initialUrl),
onCreated = { webView ->
with(webView.settings) {
javaScriptEnabled = true
domStorageEnabled = true
}
},
client = remember { SprinklrWebViewClient(state.onNewUrl) },
)
}

View file

@ -0,0 +1,10 @@
package com.tangem.tap.features.sprinklr.ui
import com.google.accompanist.web.WebContent
internal data class SprinklrScreenState(
val initialUrl: String = "",
val sprinklrDomains: List<String> = emptyList(),
val onNavigateBack: () -> Unit = {},
val onNewUrl: WebContent.(String) -> WebContent = { this },
)

View file

@ -0,0 +1,64 @@
package com.tangem.tap.features.sprinklr.ui
import androidx.lifecycle.ViewModel
import com.google.accompanist.web.WebContent
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.sprinklr.redux.SprinklrState
import com.tangem.tap.store
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import org.rekotlin.StoreSubscriber
internal class SprinklrViewModel : ViewModel(), StoreSubscriber<SprinklrState> {
private val stateInternal = MutableStateFlow(SprinklrScreenState())
val state = stateInternal.asStateFlow()
init {
subscribeToStoreChanges()
}
override fun newState(state: SprinklrState) {
stateInternal.update { prevState ->
prevState.copy(
initialUrl = state.url,
sprinklrDomains = state.sprinklrDomains,
onNewUrl = { updateWebContentOrOpenExternalUrl(it) },
)
}
}
override fun onCleared() {
store.unsubscribe(this)
}
fun setNavigateBackCallback(callback: () -> Unit) {
stateInternal.update { prevState ->
prevState.copy(
onNavigateBack = callback,
)
}
}
private fun subscribeToStoreChanges() {
store.subscribe(this) { appState ->
appState.skip { old, new -> old.sprinklrState == new.sprinklrState }
.select { it.sprinklrState }
}
}
private fun WebContent.updateWebContentOrOpenExternalUrl(url: String): WebContent {
return if (isExternalUrl(url)) {
store.dispatchOnMain(NavigationAction.OpenUrl(url))
this
} else when (this) {
is WebContent.Url -> copy(url = url)
else -> WebContent.Url(url)
}
}
private fun isExternalUrl(url: String): Boolean {
return state.value.sprinklrDomains.none { url.contains(it) }
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.features.sprinklr.ui.webview
import android.webkit.WebResourceRequest
import android.webkit.WebView
import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.WebContent
internal class SprinklrWebViewClient(
private val onNewUrl: WebContent.(String) -> WebContent,
) : AccompanistWebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
// If the url hasn't changed, this is probably an internal event like
// a javascript reload. We should let it happen.
if (view?.url == request?.url.toString()) {
return false
}
request?.let {
state.content = onNewUrl(state.content, it.url.toString())
}
return true
}
}

View file

@ -57,6 +57,11 @@ import com.tangem.tap.features.tokens.addCustomToken.compose.test.TestCasesList
import com.tangem.wallet.R
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
*/
private class AddCustomTokenScreen // for simple search
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AddCustomTokenScreen(

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.wallet.redux
import com.tangem.blockchain.common.TransactionData
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.feature.swap.domain.SwapInteractor
@ -14,6 +15,7 @@ import java.math.BigDecimal
data class WalletData(
val pendingTransactions: List<PendingTransaction> = emptyList(),
val historyTransactions: List<TransactionData>? = null,
val hashesCountVerified: Boolean? = null,
val walletAddresses: WalletAddresses? = null,
val currencyData: BalanceWidgetData = BalanceWidgetData(),

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.wallet.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
@ -81,7 +82,7 @@ data class WalletState(
walletsStores[0]
}
private val primaryWalletManager: WalletManager?
val primaryWalletManager: WalletManager?
get() = primaryWalletStore?.walletManager
val primaryWalletData: WalletData?
@ -317,6 +318,7 @@ data class Artwork(
const val MARTA_CARD_ID = "BC02"
const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png"
const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png"
const val SALT_PAY_URL = "key_for_switch_url_to_drawableId_of_salt_pay_card"
}
}

View file

@ -117,6 +117,7 @@ private fun WalletDataModel.mapToReduxModel(
?.toString(),
errorMessage = status.errorMessage,
),
historyTransactions = historyTransactions,
)
}

View file

@ -9,6 +9,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
@ -169,6 +170,7 @@ class MultiWalletMiddleware {
is WalletAction.MultiWallet.ShowWalletBackupWarning -> Unit
is WalletAction.MultiWallet.BackupWallet -> {
store.state.globalState.scanResponse?.let {
Analytics.addContext(it)
store.dispatch(GlobalAction.Onboarding.Start(it, canSkipBackup = false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.wallet.redux.middlewares
import androidx.core.os.bundleOf
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
@ -22,6 +23,7 @@ import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
import com.tangem.tap.scope
@ -49,10 +51,9 @@ class TradeCryptoMiddleware {
action: WalletAction.TradeCryptoAction.Buy,
) {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
val currency = selectedWalletData.currency
Analytics.send(Token.ButtonBuy(AnalyticsParam.CurrencyType.Currency(currency)))
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog())
return
@ -96,12 +97,12 @@ class TradeCryptoMiddleware {
private fun proceedSellAction() {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
val appCurrency = store.state.globalState.appCurrency
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
if (addresses.isEmpty()) return
val currency = selectedWalletData.currency
Analytics.send(Token.ButtonSell(AnalyticsParam.CurrencyType.Currency(currency)))
store.state.globalState.exchangeManager.getUrl(
@ -116,6 +117,17 @@ class TradeCryptoMiddleware {
}
}
private fun chooseAppropriateCurrency(walletState: WalletState): Currency? {
return if (walletState.primaryTokenData == null) {
walletState.selectedWalletData?.currency
} else {
walletState.primaryTokenData?.currency as? Currency.Token
}.guard {
store.dispatchDebugErrorNotification("Can't select an appropriate currency for a Trade action")
return null
}
}
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return

View file

@ -15,6 +15,7 @@ import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.converters.BasicEventsPreChecker
import com.tangem.tap.common.analytics.converters.BasicEventsSourceData
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.copyToClipboard
@ -249,6 +250,8 @@ class WalletMiddleware {
}
is NetworkStateChanged -> {
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
if (!action.isOnline) return
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync
if (selectedUserWallet != null) {
scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet) }
@ -380,7 +383,7 @@ class WalletMiddleware {
}
else -> {
Analytics.send(MainScreen.ButtonScanCard())
store.dispatch(WalletAction.Scan(MainScreen.CardWasScanned()))
store.dispatch(WalletAction.Scan(Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main)))
}
}
}

View file

@ -6,6 +6,8 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.common.extensions.dispatchToastNotification
import com.tangem.tap.common.extensions.getBlockchainTxHistory
import com.tangem.tap.common.extensions.getTokenTxHistory
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.redux.navigation.AppScreen
@ -54,6 +56,7 @@ class MultiWalletReducer {
blockchain.derivationPath,
),
existentialDepositString = getExistentialDeposit(walletManager),
historyTransactions = walletManager?.getBlockchainTxHistory(),
)
WalletStore(
@ -89,6 +92,7 @@ class MultiWalletReducer {
action.blockchain.derivationPath,
),
existentialDepositString = getExistentialDeposit(walletManager),
historyTransactions = walletManager?.getBlockchainTxHistory(),
)
val walletStore = WalletStore(
walletManager = walletManager,
@ -153,6 +157,7 @@ class MultiWalletReducer {
derivationPath = action.blockchain.derivationPath,
),
walletRent = findWalletRent(state.getWalletStore(walletManager.wallet)),
historyTransactions = walletManager.getTokenTxHistory(action.token),
)
state.updateWalletData(newTokenWalletData)
}
@ -206,8 +211,8 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat
val currency = Currency.fromBlockchainNetwork(blockchain, this)
if (state.currencies.contains(currency)) return null
val walletManager = state.getWalletManager(currency)?.wallet
val walletAddresses = createAddressList(walletManager)
val walletManager = state.getWalletManager(currency)
val walletAddresses = createAddressList(walletManager?.wallet)
return WalletData(
currencyData = BalanceWidgetData(
@ -218,5 +223,6 @@ fun Token.toWallet(state: WalletState, blockchain: BlockchainNetwork): WalletDat
walletAddresses = walletAddresses,
mainButton = WalletMainButton.SendButton(false),
currency = currency,
historyTransactions = walletManager?.getTokenTxHistory(this),
)
}

View file

@ -5,7 +5,9 @@ import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.ui.BalanceStatus
import java.math.BigDecimal
fun List<WalletData>.findProgressState(): ProgressState {
fun List<WalletData>.findProgressState(initialState: ProgressState = ProgressState.Done): ProgressState {
if (this.isEmpty()) return initialState
return this
.mapToProgressState()
.reduce(ProgressState::or)

View file

@ -348,6 +348,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
}
is WalletAction.WalletStoresChanged.UpdateWalletStores -> {
newState = newState.copy(
state = action.reduxWalletStores.flatMap { it.walletsData }.findProgressState(newState.state),
walletsStores = action.reduxWalletStores,
)
}

View file

@ -40,9 +40,9 @@ import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView
import com.tangem.tap.features.wallet.ui.wallet.SaltPayWalletView
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.features.wallet.ui.wallet.saltPay.SaltPayWalletView
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.wallet.BuildConfig
@ -155,19 +155,22 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun newState(state: WalletState) {
if (activity == null || view == null) return
val isSaltPay = store.state.globalState.scanResponse?.card?.isSaltPay == true
val isSaltPay = store.state.globalState.scanResponse?.cardTypesResolver?.isSaltPay() == true
when {
isSaltPay && walletView !is SaltPayWalletView -> {
walletView.onViewDestroy()
walletView = SaltPayWalletView()
walletView.changeWalletView(this, binding)
}
state.isMultiwalletAllowed && state.primaryWalletData?.currencyData?.status != BalanceStatus.EmptyCard &&
walletView !is MultiWalletView -> {
walletView.onViewDestroy()
walletView = MultiWalletView()
walletView.changeWalletView(this, binding)
}
!state.isMultiwalletAllowed && !isSaltPay && walletView !is SingleWalletView -> {
walletView.onViewDestroy()
walletView = SingleWalletView()
walletView.changeWalletView(this, binding)
}
@ -185,7 +188,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
setupNoInternetHandling(state)
setupCardImage(state)
setupCardImage(state, isSaltPay)
if (!isSaltPay) showWarningsIfPresent(state.mainWarningsList)
@ -222,9 +225,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
}
private fun setupCardImage(state: WalletState) {
// TODO: SaltPay: remove hardCode
if (store.state.globalState.scanResponse?.cardTypesResolver?.isSaltPay() == true) {
private fun setupCardImage(state: WalletState, isSaltPay: Boolean) {
//TODO: SaltPay: remove hardCode
if (isSaltPay) {
binding.ivCard.load(R.drawable.img_salt_pay_visa) {
scale(Scale.FIT)
crossfade(enable = true)

View file

@ -1,88 +0,0 @@
package com.tangem.tap.features.wallet.ui.wallet
import com.tangem.domain.common.extensions.debounce
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.databinding.FragmentWalletBinding
import com.tangem.wallet.databinding.LayoutSaltPayWalletBinding
import org.rekotlin.Action
class SaltPayWalletView : WalletView() {
private lateinit var saltPayBinding: LayoutSaltPayWalletBinding
private val actionDebouncer = debounce<Action>(500, mainScope) { store.dispatch(it) }
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
saltPayBinding = binding.lSaltPayWallet
setFragment(fragment, binding)
onViewCreated()
showSaltPayView(binding)
}
private fun showSaltPayView(binding: FragmentWalletBinding) = with(binding) {
rvWarningMessages.hide()
rvPendingTransaction.hide()
rvMultiwallet.hide()
tvTwinCardNumber.hide()
lCardBalance.root.hide()
lSingleWalletBalance.root.hide()
lAddress.root.hide()
rowButtons.hide()
btnAddToken.hide()
pbLoadingUserTokens.hide()
lSaltPayWallet.root.show()
}
override fun onViewCreated() {
}
override fun onNewState(state: WalletState) {
setupBalanceWidget(state)
}
private fun setupBalanceWidget(state: WalletState) = with(saltPayBinding.lSaltPayBalance) {
val tokenData = state.primaryTokenData ?: return@with
val appCurrency = store.state.globalState.appCurrency
val mainProgressState = state.state
if (mainProgressState == ProgressState.Loading) {
veilBalance.veil()
veilBalanceCrypto.veil()
} else {
veilBalanceCrypto.unVeil()
}
tvProcessing.animateVisibility(show = mainProgressState == ProgressState.Error)
veilBalanceCrypto.animateVisibility(show = mainProgressState != ProgressState.Error)
if (tokenData.currencyData.fiatAmount == null) {
actionDebouncer(WalletAction.LoadFiatRate())
} else {
veilBalance.unVeil()
tvBalance.text = tokenData.currencyData.fiatAmount.formatAmountAsSpannedString(
currencySymbol = appCurrency.symbol,
)
}
tvBalanceCrypto.text = tokenData.currencyData.amountFormatted
tvCurrencyName.text = appCurrency.code
tvCurrencyName.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
btnBuy.show(tokenData.isAvailableToBuy(store.state.globalState.exchangeManager))
btnBuy.setOnClickListener {
store.dispatch(WalletAction.TradeCryptoAction.Buy(false))
}
}
}

View file

@ -6,10 +6,12 @@ import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.wallet.databinding.FragmentWalletBinding
abstract class WalletView {
var swapInteractor: SwapInteractor? = null
protected var fragment: WalletFragment? = null
protected var binding: FragmentWalletBinding? = null
fun setFragment(fragment: WalletFragment, binding: FragmentWalletBinding) {
this.fragment = fragment
this.binding = binding
@ -20,6 +22,10 @@ abstract class WalletView {
binding = null
}
open fun onViewDestroy() {
removeFragment()
}
open fun onDestroyFragment() {}
abstract fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding)

View file

@ -0,0 +1,21 @@
package com.tangem.tap.features.wallet.ui.wallet.saltPay
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.tap.features.wallet.models.PendingTransactionType
/**
[REDACTED_AUTHOR]
*/
data class HistoryTransactionData(
val transactionData: TransactionData,
private val walletAddress: String,
) {
fun isInProgress(): Boolean = transactionData.status == TransactionStatus.Unconfirmed
fun getTransactionType(): PendingTransactionType = when {
transactionData.sourceAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Outgoing
transactionData.destinationAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Incoming
else -> PendingTransactionType.Unknown
}
}

View file

@ -0,0 +1,260 @@
package com.tangem.tap.features.wallet.ui.wallet.saltPay
import android.view.LayoutInflater
import android.view.ViewGroup
import com.skydoves.androidveil.VeilLayout
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.debounce
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.ShimmerData
import com.tangem.tap.common.ShimmerRecyclerAdapter
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryItemData
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.HistoryTransactionData
import com.tangem.tap.features.wallet.ui.wallet.saltPay.rv.TxHistoryAdapter
import com.tangem.tap.mainScope
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.databinding.FragmentWalletBinding
import com.tangem.wallet.databinding.ItemSaltPayTxHistoryShimmerBinding
import com.tangem.wallet.databinding.LayoutSaltPayBalanceBinding
import com.tangem.wallet.databinding.LayoutSaltPayTxHistoryBinding
import com.tangem.wallet.databinding.LayoutSaltPayWalletBinding
import kotlinx.coroutines.launch
import org.rekotlin.Action
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class SaltPayWalletView : WalletView() {
private var saltPayBinding: LayoutSaltPayWalletBinding? = null
private val actionDebouncer = debounce<Action>(500, mainScope) { store.dispatch(it) }
private val balanceWidget: LayoutSaltPayBalanceBinding?
get() = saltPayBinding?.lSaltPayBalance
private val txWidget: LayoutSaltPayTxHistoryBinding?
get() = saltPayBinding?.lSaltPayTxHistory
private val itemDecorator = SpaceItemDecoration.vertical(10F)
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
Timber.d("changeWalletView")
saltPayBinding = binding.lSaltPayWallet
showSaltPayView(binding)
onViewCreated()
}
override fun onViewCreated() {
Timber.d("onViewCreated")
prepareTxRecyclers(txWidget!!)
}
private fun showSaltPayView(binding: FragmentWalletBinding) = with(binding) {
Timber.d("showSaltPayView")
rvWarningMessages.hide()
rvPendingTransaction.hide()
rvMultiwallet.hide()
tvTwinCardNumber.hide()
lCardBalance.root.hide()
lSingleWalletBalance.root.hide()
lAddress.root.hide()
rowButtons.hide()
btnAddToken.hide()
pbLoadingUserTokens.hide()
lSaltPayWallet.root.show()
}
private fun prepareTxRecyclers(txWidget: LayoutSaltPayTxHistoryBinding) = with(txWidget) {
Timber.d("prepareTxRecyclers")
val vhViewFactory: (ViewGroup) -> ViewGroup = {
val inflater = LayoutInflater.from(it.context)
ItemSaltPayTxHistoryShimmerBinding.inflate(inflater, it, false).root
}
val adapter = ShimmerRecyclerAdapter(vhViewFactory)
rvTxHistoryShimmer.adapter = adapter
rvTxHistoryShimmer.addItemDecoration(itemDecorator)
adapter.submitList(
listOf(
ShimmerData(),
ShimmerData(),
ShimmerData(),
),
)
rvTxHistory.adapter = TxHistoryAdapter()
rvTxHistory.addItemDecoration(itemDecorator)
}
override fun onNewState(state: WalletState) {
val balanceWidget = balanceWidget ?: return
val txWidget = txWidget ?: return
val exchangeManager = store.state.globalState.exchangeManager
setupBalanceWidget(balanceWidget, exchangeManager, state)
setupTxHistoryWidget(txWidget, state)
}
private fun setupBalanceWidget(
balanceWidget: LayoutSaltPayBalanceBinding,
exchangeManager: CurrencyExchangeManager,
state: WalletState,
) = with(balanceWidget) {
Timber.d("setupBalanceWidget")
val tokenData = state.primaryTokenData ?: return
val appCurrency = store.state.globalState.appCurrency
val mainProgressState = state.state
if (mainProgressState == ProgressState.Loading) {
veilBalance.veil()
veilBalanceCrypto.veil()
} else {
veilBalanceCrypto.unVeil()
}
tvUnreachable.animateVisibility(show = mainProgressState == ProgressState.Error)
veilBalanceCrypto.animateVisibility(show = mainProgressState != ProgressState.Error)
if (tokenData.currencyData.fiatAmount == null) {
veilBalance.veil()
actionDebouncer(WalletAction.LoadFiatRate())
} else {
veilBalance.unVeil()
tvBalance.text = tokenData.currencyData.fiatAmount.formatAmountAsSpannedString(
currencySymbol = appCurrency.symbol,
)
}
tvBalanceCrypto.text = tokenData.currencyData.amountFormatted
tvCurrencyName.text = appCurrency.code
tvCurrencyName.setOnClickListener {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
btnBuy.show(tokenData.isAvailableToBuy(exchangeManager))
btnBuy.setOnClickListener {
Analytics.send(MainScreen.ButtonBuy())
store.dispatch(WalletAction.TradeCryptoAction.Buy(false))
}
}
private fun setupTxHistoryWidget(txWidget: LayoutSaltPayTxHistoryBinding, state: WalletState) {
Timber.d("setupTxHistoryWidget")
if (state.state == ProgressState.Loading) {
handleTxLoading(txWidget)
return
}
if (state.state == ProgressState.Error) {
handleTxError(txWidget)
return
}
if (state.state != ProgressState.Done) return
val walletAddress = state.primaryWalletManager?.wallet?.address ?: return
val tokenData = state.primaryTokenData ?: return
val historyTransactions = tokenData.historyTransactions ?: return
if (historyTransactions.isEmpty()) {
handleTxEmpty(txWidget)
return
}
scope.launch {
val dateAssociatedHistory = mutableListOf<Pair<String, MutableList<HistoryTransactionData>>>()
historyTransactions.forEach { tx ->
val txHistoryData = HistoryTransactionData(tx, walletAddress)
val item = dateAssociatedHistory.firstOrNull { it.first == txHistoryData.date }
if (item == null) {
dateAssociatedHistory.add(Pair(txHistoryData.date, mutableListOf(txHistoryData)))
} else {
item.second.add(txHistoryData)
}
}
val rvDataList = mutableListOf<HistoryItemData>()
dateAssociatedHistory.forEach { (txData, txHistoryData) ->
rvDataList.add(HistoryItemData.Date(txData))
rvDataList.addAll(txHistoryData.map { HistoryItemData.TransactionData(it) })
}
withMainContext { handleTxSuccess(txWidget, rvDataList) }
}
}
private fun handleTxLoading(txWidget: LayoutSaltPayTxHistoryBinding) = with(txWidget) {
Timber.d("handleTxLoading")
groupEmpty.hide()
groupError.hide()
groupSuccess.hide()
// root.beginDelayedTransition()
groupShimmer.show()
}
private fun handleTxSuccess(
txWidget: LayoutSaltPayTxHistoryBinding,
dataList: List<HistoryItemData>,
) = with(txWidget) {
Timber.d("handleTxSuccess")
groupShimmer.hide()
groupEmpty.hide()
groupError.hide()
// root.beginDelayedTransition()
updateTxHistoryWidget(txWidget, dataList)
groupSuccess.show()
}
private fun handleTxEmpty(txWidget: LayoutSaltPayTxHistoryBinding) = with(txWidget) {
Timber.d("handleTxEmpty")
groupShimmer.hide()
groupError.hide()
groupSuccess.hide()
// root.beginDelayedTransition()
updateTxHistoryWidget(txWidget, emptyList())
groupEmpty.show()
}
private fun handleTxError(txWidget: LayoutSaltPayTxHistoryBinding) = with(txWidget) {
Timber.d("handleTxError")
groupShimmer.hide()
groupEmpty.hide()
groupSuccess.hide()
// root.beginDelayedTransition()
updateTxHistoryWidget(txWidget, emptyList())
groupError.show()
}
private fun updateTxHistoryWidget(
txWidget: LayoutSaltPayTxHistoryBinding,
dataList: List<HistoryItemData>,
) = with(txWidget) {
(rvTxHistory.adapter as TxHistoryAdapter).submitList(dataList)
}
override fun onViewDestroy() {
Timber.d("onViewDestroy")
txWidget?.apply {
rvTxHistoryShimmer.removeItemDecoration(itemDecorator)
rvTxHistory.removeItemDecoration(itemDecorator)
}
super.onViewDestroy()
}
}
private fun VeilLayout.changeVeilState(show: Boolean) {
if (show) this.veil()
else this.unVeil()
}

View file

@ -0,0 +1,78 @@
package com.tangem.tap.features.wallet.ui.wallet.saltPay.rv
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.common.extensions.guard
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.features.wallet.models.PendingTransactionType
import java.text.SimpleDateFormat
import java.util.*
/**
[REDACTED_AUTHOR]
*/
sealed class HistoryItemData(
val viewType: Int,
val itemId: Long,
) {
data class Date(val date: String) : HistoryItemData(0, date.hashCode().toLong())
data class TransactionData(val data: HistoryTransactionData) : HistoryItemData(1, data.address.hashCode().toLong())
}
data class HistoryTransactionData(
private val transactionData: TransactionData,
private val walletAddress: String,
) {
val isInProgress: Boolean = transactionData.status == TransactionStatus.Unconfirmed
val transactionType: PendingTransactionType = when {
transactionData.sourceAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Outgoing
transactionData.destinationAddress.lowercase() == walletAddress.lowercase() -> PendingTransactionType.Incoming
else -> PendingTransactionType.Unknown
}
val address: String = when (transactionType) {
PendingTransactionType.Incoming -> transactionData.sourceAddress.reduceAddress()
PendingTransactionType.Outgoing -> transactionData.destinationAddress.reduceAddress()
PendingTransactionType.Unknown -> "Unknown address"
}
val time: String = transactionData.date?.let {
SimpleDateFormat("HH:mm").format(it.time)
} ?: "00:00"
val date: String = calculateDate()
val txSign: String = when (transactionType) {
PendingTransactionType.Incoming -> "+"
PendingTransactionType.Outgoing -> "-"
PendingTransactionType.Unknown -> ""
}
val amountValue: String = transactionData.amount.value
?.toFormattedCurrencyString(8, transactionData.amount.currencySymbol) ?: "0"
private fun calculateDate(): String {
val calendarTx = transactionData.date.guard { return "Unknown" }
val calendarNow = Calendar.getInstance()
val oldDateFormatter = SimpleDateFormat("dd.MM.yyyy")
val date = if (calendarTx.get(Calendar.YEAR) == calendarNow.get(Calendar.YEAR)) {
val dayOfYearTx = calendarTx.get(Calendar.DAY_OF_YEAR)
val dayOfYearNow = calendarNow.get(Calendar.DAY_OF_YEAR)
when {
dayOfYearNow == dayOfYearTx -> "Today"
dayOfYearNow - dayOfYearTx == 1 -> "Tomorrow"
else -> oldDateFormatter.format(calendarTx.time)
}
} else {
oldDateFormatter.format(calendarTx.time)
}
return date
}
private fun String.reduceAddress(): String = "${substring(0..5)}...${substring(length - 4)}"
}

View file

@ -0,0 +1,33 @@
package com.tangem.tap.features.wallet.ui.wallet.saltPay.rv
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
/**
[REDACTED_AUTHOR]
*/
open class ShimmerRecyclerAdapter(
private val viewHolderViewFactory: (ViewGroup) -> ViewGroup,
) : ListAdapter<ShimmerData, ShimmerVH>(DiffUtilCallback) {
override fun getItemId(position: Int): Long {
return if (currentList.isEmpty()) 0 else currentList[position].hashCode().toLong()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShimmerVH {
return ShimmerVH(viewHolderViewFactory.invoke(parent))
}
override fun onBindViewHolder(holder: ShimmerVH, position: Int) {}
object DiffUtilCallback : DiffUtil.ItemCallback<ShimmerData>() {
override fun areContentsTheSame(oldItem: ShimmerData, newItem: ShimmerData) = oldItem == newItem
override fun areItemsTheSame(oldItem: ShimmerData, newItem: ShimmerData) = oldItem == newItem
}
}
class ShimmerVH(viewGroup: ViewGroup) : RecyclerView.ViewHolder(viewGroup)
data class ShimmerData(private val any: String = "")

View file

@ -0,0 +1,112 @@
package com.tangem.tap.features.wallet.ui.wallet.saltPay.rv
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.setDrawable
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ItemSaltPayTxHistoryBinding
import com.tangem.wallet.databinding.ItemSaltPayTxHistoryDateBinding
/**
[REDACTED_AUTHOR]
*/
internal class TxHistoryAdapter : ListAdapter<HistoryItemData, BaseTxHistoryVH>(DiffUtilCallback) {
override fun getItemId(position: Int): Long {
return if (currentList.isEmpty()) 0L
else currentList[position].itemId
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseTxHistoryVH {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
0 -> {
val layout = ItemSaltPayTxHistoryDateBinding.inflate(inflater, parent, false)
DateItemVH(layout)
}
1 -> {
val layout = ItemSaltPayTxHistoryBinding.inflate(inflater, parent, false)
TransactionItemVH(layout)
}
else -> throw UnsupportedOperationException()
}
}
override fun getItemViewType(position: Int): Int = currentList[position].viewType
override fun onBindViewHolder(holder: BaseTxHistoryVH, position: Int) {
holder.bind(currentList[position])
}
private object DiffUtilCallback : DiffUtil.ItemCallback<HistoryItemData>() {
override fun areContentsTheSame(oldItem: HistoryItemData, newItem: HistoryItemData) = oldItem == newItem
override fun areItemsTheSame(oldItem: HistoryItemData, newItem: HistoryItemData) = oldItem == newItem
}
}
internal abstract class BaseTxHistoryVH(layout: View) : RecyclerView.ViewHolder(layout) {
abstract fun bind(data: HistoryItemData)
}
private class DateItemVH(
private val binding: ItemSaltPayTxHistoryDateBinding,
) : BaseTxHistoryVH(binding.root) {
override fun bind(data: HistoryItemData) = with(binding) {
val vhData = (data as? HistoryItemData.Date)?.date ?: return
tvTitleDate.text = vhData
}
}
private class TransactionItemVH(
private val binding: ItemSaltPayTxHistoryBinding,
) : BaseTxHistoryVH(binding.root) {
override fun bind(data: HistoryItemData) {
val vhData = (data as? HistoryItemData.TransactionData)?.data ?: return
setupImage(vhData)
setupTitle(vhData)
setupSubtitle(vhData)
setupBalance(vhData)
}
private fun setupImage(data: HistoryTransactionData) = with(binding) {
val drawable = when (data.transactionType) {
PendingTransactionType.Incoming -> R.drawable.ic_tx_incoming
PendingTransactionType.Outgoing -> R.drawable.ic_tx_outgoing
PendingTransactionType.Unknown -> null
}
drawable?.let { imvTx.setDrawable(it) }
if (data.isInProgress) {
imvTx.setColorFilter(imvTx.getColor(R.color.icon_attention), PorterDuff.Mode.SRC_ATOP)
} else {
imvTx.clearColorFilter()
}
}
private fun setupTitle(data: HistoryTransactionData) = with(binding) {
tvTxHash.text = data.address
}
private fun setupSubtitle(data: HistoryTransactionData) = with(binding) {
tvTxStatus.show(data.isInProgress)
tvTxTime.show(!data.isInProgress)
tvTxTime.text = data.time
}
private fun setupBalance(data: HistoryTransactionData) = with(binding) {
tvTxAmountSign.text = data.txSign
tvTxAmountValue.text = data.amountValue
}
}

View file

@ -9,8 +9,9 @@ import com.tangem.common.map
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
@ -140,7 +141,7 @@ internal class WalletSelectorMiddleware {
)
ScanCardProcessor.scan(
analyticsEvent = MyWallets.CardWasScanned(),
analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.MyWallets),
onWalletNotCreated = {
// No need to rollback policy, continue with the policy set before the card scan
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
@ -209,9 +210,6 @@ internal class WalletSelectorMiddleware {
.doOnSuccess {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
val batchId = selectedUserWallet.scanResponse.card.batchId
Analytics.addParamsInterceptor(BatchIdParamsInterceptor(batchId))
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedUserWallet)
}

View file

@ -47,7 +47,10 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
@OptIn(ExperimentalComposeUiApi::class)
@Composable
override fun ScreenContent(state: WalletSelectorScreenState, modifier: Modifier) {
override fun ScreenContent(
modifier: Modifier,
state: WalletSelectorScreenState,
) {
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val dialog by rememberUpdatedState(newValue = state.dialog)

View file

@ -4,7 +4,17 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
@ -29,6 +39,7 @@ import com.tangem.core.ui.components.SpacerW6
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.common.extensions.cardImageData
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem
@ -100,7 +111,7 @@ private fun WalletCardImage(
SubcomposeAsyncImage(
modifier = cardImageModifier,
model = ImageRequest.Builder(LocalContext.current)
.data(cardImageUrl)
.cardImageData(cardImageUrl)
.crossfade(true)
.build(),
loading = { CardImageShimmer(modifier = cardImageModifier) },

View file

@ -5,6 +5,7 @@ import com.tangem.common.core.TangemError
import org.rekotlin.Action
internal sealed interface WelcomeAction : Action {
object OnCreate : WelcomeAction
object ProceedWithBiometrics : WelcomeAction {
object Success : WelcomeAction
data class Error(val error: TangemError) : WelcomeAction

View file

@ -4,9 +4,13 @@ import android.content.Intent
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.eraseContext
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
@ -38,6 +42,10 @@ internal class WelcomeMiddleware {
private fun handleAction(action: WelcomeAction, state: WelcomeState) {
when (action) {
is WelcomeAction.OnCreate -> {
Analytics.eraseContext()
Analytics.send(SignIn.ScreenOpened())
}
is WelcomeAction.ProceedWithBiometrics -> {
proceedWithBiometrics(state)
}
@ -107,7 +115,7 @@ internal class WelcomeMiddleware {
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
)
ScanCardProcessor.scan(
analyticsEvent = SignIn.CardWasScanned(),
analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.SignIn),
onSuccess = { scanResponse ->
scope.launch { onCardScanned(scanResponse) }
},

View file

@ -26,6 +26,7 @@ internal object WelcomeReducer {
is WelcomeAction.ProceedWithBiometrics.Success -> state.copy(isUnlockWithBiometricsInProgress = false)
is WelcomeAction.ProceedWithCard.Success -> state.copy(isUnlockWithCardInProgress = false)
is WelcomeAction.CloseError -> state.copy(error = null)
else -> state
}
}
}

View file

@ -19,14 +19,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.fragment.app.viewModels
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.fragments.ComposeFragment
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.features.welcome.ui.components.BiometricsLockoutDialog
import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent
import com.tangem.tap.store
import com.tangem.wallet.R
internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
@ -34,7 +34,7 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(SignIn.ScreenOpened())
store.dispatch(WelcomeAction.OnCreate)
}
@Composable
@ -43,7 +43,10 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
}
@Composable
override fun ScreenContent(state: WelcomeScreenState, modifier: Modifier) {
override fun ScreenContent(
modifier: Modifier,
state: WelcomeScreenState,
) {
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val biometricsLockoutDialog by rememberUpdatedState(newValue = state.biometricsLockoutDialog)