Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-13 14:57:13 +08:00
commit b06f7248b4
201 changed files with 3172 additions and 7126 deletions

View file

@ -5,15 +5,17 @@ import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.Snackbar
import com.tangem.TangemSdk
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.wallet.navigation.WalletRouter
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.extensions.init
import com.tangem.sdk.extensions.initWithBiometrics
import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback
@ -77,6 +79,18 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var testerRouter: TesterRouter
@Inject
lateinit var injectedTangemSdk: TangemSdk
@Inject
lateinit var injectedTangemSdkManager: TangemSdkManager
@Inject
lateinit var scanCardUseCase: ScanCardUseCase
@Inject
lateinit var walletRouter: WalletRouter
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
@ -84,13 +98,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
systemActions()
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
tangemSdk = TangemSdk.initWithBiometrics(this, TangemSdkManager.config)
tangemSdkManager = TangemSdkManager(tangemSdk, this)
tangemSdk = injectedTangemSdk
tangemSdkManager = injectedTangemSdkManager
appStateHolder.tangemSdkManager = tangemSdkManager
appStateHolder.tangemSdk = tangemSdk
backupService = BackupService.init(tangemSdk, this)
@ -104,7 +120,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
),
)
store.dispatch(DaggerGraphAction.SetActivityDependencies(testerRouter))
store.dispatch(
DaggerGraphAction.SetActivityDependencies(
testerRouter = testerRouter,
scanCardUseCase = scanCardUseCase,
walletRouter = walletRouter,
),
)
}
private fun initUserWalletsListManager() {

View file

@ -21,6 +21,7 @@ import com.tangem.datasource.config.models.Config
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
@ -133,6 +134,9 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var preferencesDataSource: PreferencesDataSource
@Inject
lateinit var walletFeatureToggles: WalletFeatureToggles
override fun onCreate() {
super.onCreate()
@ -146,6 +150,7 @@ class TapApplication : Application(), ImageLoaderFactory {
assetReader = assetReader,
networkConnectionManager = networkConnectionManager,
customTokenFeatureToggles = customTokenFeatureToggles,
walletFeatureToggles = walletFeatureToggles,
),
),
)

View file

@ -12,7 +12,6 @@ import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.TwinningProcessNotCompletedDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.*
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.*
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.ui.dialogs.*
@ -118,9 +117,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
context = context,
cardId = state.dialog.cardId,
)
is SaltPayDialog.Activation.NoGas -> NoFundsForActivationDialog.create(context)
is SaltPayDialog.Activation.PutVisaCard -> PutVisaCardDialog.create(context)
is SaltPayDialog.Activation.OnError -> RegistrationErrorDialog.create(context, state.dialog)
is WalletDialog.CurrencySelectionDialog -> CurrencySelectionDialog.create(state.dialog, context)
is WalletDialog.ChooseTradeActionDialog -> ChooseTradeActionBottomSheetDialog(context, state.dialog)
is WalletDialog.SelectAmountToSendDialog -> AmountToSendBottomSheetDialog(context, state.dialog)

View file

@ -3,7 +3,6 @@ package com.tangem.tap.common.analytics.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.Converter
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.tap.common.analytics.events.AnalyticsParam
/**
@ -17,7 +16,6 @@ class ParamCardCurrencyConverter : Converter<CardTypesResolver, AnalyticsParam.C
val type = when {
value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
value.isSaltPay() -> AnalyticsParam.CurrencyType.Token(SaltPayWorkaround.tokenFrom(Blockchain.SaltPay))
value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!)
else -> null

View file

@ -45,11 +45,6 @@ class CardContextInterceptor(
ProductType.Note -> "Note"
ProductType.Twins -> "Twin"
ProductType.Wallet -> "Wallet"
ProductType.SaltPay -> if (scanResponse.cardTypesResolver.isSaltPayVisa()) {
"Visa"
} else {
"Visa Backup"
}
ProductType.Start2Coin -> "Start2Coin"
else -> if (DemoHelper.isDemoCard(scanResponse)) {
if (DemoHelper.isTestDemoCard(scanResponse)) {

View file

@ -1,12 +0,0 @@
package com.tangem.tap.common.extensions
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.operations.backup.BackupService
/**
[REDACTED_AUTHOR]
*/
@Suppress("MagicNumber")
fun BackupService.primaryCardIsSaltPayVisa(): Boolean {
return primaryCardId?.slice(0..3)?.let(SaltPayWorkaround::isVisaBatchId) ?: false
}

View file

@ -1,32 +1,10 @@
package com.tangem.tap.common.extensions
import android.widget.ImageView
import coil.ImageLoader
import coil.imageLoader
import coil.load
import coil.request.Disposable
import coil.request.ImageRequest
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
fun ImageRequest.Builder.cardImageData(any: Any?): ImageRequest.Builder = apply {
data(any?.parseData())
}
fun ImageView.loadCardImage(
data: Any?,
imageLoader: ImageLoader = context.imageLoader,
builder: ImageRequest.Builder.() -> Unit = {},
): Disposable = this.load(
data = data?.parseData(),
imageLoader = imageLoader,
builder = builder,
)
private fun Any?.parseData(): Any? = when {
this is String && this == Artwork.SALT_PAY_URL -> R.drawable.img_salt_pay_visa
else -> this
data(any)
}

View file

@ -1,11 +1,7 @@
package com.tangem.tap.common.extensions
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.*
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.tap.common.redux.navigation.AppScreen
@ -138,7 +134,18 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> TwinsCardsFragment()
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
AppScreen.Wallet -> WalletFragment()
AppScreen.Wallet -> {
val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::walletFeatureToggles,
)
if (featureToggles.isRedesignedScreenEnabled) {
store.state.daggerGraphState
.get(getDependency = DaggerGraphState::walletRouter)
.getEntryFragment()
} else {
WalletFragment()
}
}
AppScreen.Send -> SendFragment()
AppScreen.Details -> DetailsFragment()
AppScreen.DetailsSecurity -> SecurityModeFragment()

View file

@ -2,9 +2,6 @@ package com.tangem.tap.common.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionHistoryProvider
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.services.Result
@ -38,9 +35,6 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
Result.Success(wallet)
} else {
update()
if (wallet.blockchain == Blockchain.SaltPay && this is TransactionHistoryProvider) {
this.getTransactionHistory(wallet.address, wallet.blockchain, wallet.getTokens())
}
Result.Success(wallet)
}
} catch (exception: Exception) {
@ -87,26 +81,10 @@ fun WalletManager?.getAddressData(): WalletDataModel.AddressData? {
return if (addressDataList.isEmpty()) null else addressDataList[0]
}
fun WalletManager.getBlockchainTxHistory(): List<TransactionData> = wallet.getBlockchainTxHistory()
fun WalletManager.getTokenTxHistory(token: Token): List<TransactionData> = wallet.getTokenTxHistory(token)
fun <T> WalletManager.Companion.stub(): T {
val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null, null), setOf())
return object : WalletManager(wallet) {
override val currentHost: String = ""
override suspend fun update() {}
} as T
}
fun Wallet.getBlockchainTxHistory(): List<TransactionData> {
return historyTransactions.filter {
it.contractAddress.isNullOrEmpty()
}
}
fun Wallet.getTokenTxHistory(token: Token): List<TransactionData> {
return historyTransactions.filter {
it.contractAddress == token.contractAddress
}
}

View file

@ -24,7 +24,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
@ -94,7 +93,6 @@ data class AppState(
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
OnboardingSaltPayMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
WalletMiddleware().walletMiddleware,
TwinCardsMiddleware.handler,

View file

@ -38,19 +38,16 @@ sealed class GlobalAction : Action {
sealed class Onboarding : GlobalAction() {
/**
* Initiate an onboarding process.
* For resuming unfinished backup of standard Wallet and SaltPay cards see
* For resuming unfinished backup of standard Wallet see
* BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup
*/
data class Start(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : Onboarding()
/**
* Initiate resuming of unfinished backup for standard Wallet and SaltPay cards.
* Initiate resuming of unfinished backup for standard Wallet.
* See more BackupAction.CheckForUnfinishedBackup
*/
data class StartForUnfinishedBackup(
val addedBackupCardsCount: Int,
val isSaltPayVisa: Boolean,
) : Onboarding()
data class StartForUnfinishedBackup(val addedBackupCardsCount: Int) : Onboarding()
object Stop : Onboarding()
}

View file

@ -6,9 +6,7 @@ import com.tangem.common.extensions.guard
import com.tangem.datasource.config.models.Config
import com.tangem.domain.common.LogConfig
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
@ -26,16 +24,12 @@ import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoEnvironment
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.network.exchangeServices.utorg.UtorgAuthProvider
import com.tangem.tap.network.exchangeServices.utorg.UtorgEnvironment
import com.tangem.tap.network.exchangeServices.utorg.UtorgExchangeService
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import com.tangem.tap.walletCurrenciesManager
import com.tangem.wallet.BuildConfig
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -109,13 +103,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
return
}
val scanResponse = globalState.scanResponse ?: globalState.onboardingState.onboardingManager?.scanResponse
// if config not set -> try to get it based on a scanResponse.productType
val unsafeChatConfig = action.chatConfig ?: when {
scanResponse?.cardTypesResolver?.isSaltPay() == true -> config.saltPayConfig?.sprinklr
else -> config.zendesk
}
val unsafeChatConfig = action.chatConfig ?: config.zendesk
val chatConfig = unsafeChatConfig.guard {
store.dispatchDebugErrorNotification("ZendeskConfig not initialized")
@ -139,11 +128,10 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
store.state.globalState.scanResponse
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
}
val productTypeProvider: () -> ProductType? = { scanResponseProvider.invoke()?.productType }
val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card }
val exchangeManager = CurrencyExchangeManager(
buyService = makeBuyExchangeService(config, productTypeProvider),
buyService = makeBuyExchangeService(config),
sellService = makeSellExchangeService(config),
primaryRules = CardExchangeRules(cardProvider),
)
@ -213,28 +201,13 @@ private fun makeSellExchangeService(config: Config): ExchangeService {
)
}
private fun makeBuyExchangeService(config: Config, productTypeProvider: () -> ProductType?): ExchangeService {
private fun makeBuyExchangeService(config: Config): ExchangeService {
return BuyExchangeService(
productTypeProvider = productTypeProvider,
mercuryoService = makeMercuryoExchangeService(config),
utorgService = makeUtorgExchangeService(config),
)
}
private fun makeMercuryoExchangeService(config: Config): MercuryoService {
val mercuryoEnvironment = MercuryoEnvironment.prod(config.mercuryoWidgetId, config.mercuryoSecret)
return MercuryoService(mercuryoEnvironment)
}
private fun makeUtorgExchangeService(config: Config): UtorgExchangeService {
val saltPayConfig = requireNotNull(config.saltPayConfig)
val utorgAuthProvider = UtorgAuthProvider(saltPayConfig.kycProvider.sidValue)
val utorgEnvironment = if (BuildConfig.DEBUG) {
UtorgEnvironment.stage(utorgAuthProvider, LogConfig.network.utorgService)
// UtorgEnvironment.mock()
} else {
UtorgEnvironment.prod(utorgAuthProvider)
}
return UtorgExchangeService(utorgEnvironment)
}

View file

@ -0,0 +1,42 @@
package com.tangem.tap.di
import android.content.Context
import androidx.fragment.app.FragmentActivity
import com.tangem.TangemSdk
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.sdk.extensions.initWithBiometrics
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.userTokensRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
@Module
@InstallIn(ActivityComponent::class)
internal object ActivityModule {
@Provides
@ActivityScoped
fun provideTangemSdk(@ActivityContext context: Context): TangemSdk {
return TangemSdk.initWithBiometrics(context as FragmentActivity, TangemSdkManager.config)
}
@Provides
@ActivityScoped
fun provideTangemSdkManager(@ActivityContext context: Context, tangemSdk: TangemSdk): TangemSdkManager {
return TangemSdkManager(tangemSdk, context)
}
@Provides
@ActivityScoped
fun provideScanCardUseCase(tangemSdk: TangemSdk, tangemSdkManager: TangemSdkManager): ScanCardUseCase {
return ScanCardUseCase(
tangemSdk = tangemSdk,
scanCardRepository = DefaultScanCardRepository(userTokensRepository, tangemSdkManager),
)
}
}

View file

@ -229,7 +229,6 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
tangemSdk.config.cardIdDisplayFormat = when {
scanResponse == null -> CardIdDisplayFormat.Full
scanResponse.cardTypesResolver.isTangemTwins() -> CardIdDisplayFormat.LastLuhn(4)
scanResponse.cardTypesResolver.isSaltPay() -> CardIdDisplayFormat.None
else -> CardIdDisplayFormat.Full
}
}

View file

@ -22,6 +22,10 @@ sealed class TapError(
object UnknownError : TapError(R.string.send_error_unknown)
open class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage))
data class UnsupportedState(
val stateError: String,
) : TapError(R.string.common_custom_string, listOf("Unsupported state: $stateError"))
object ScanCardError : TapError(R.string.scan_card_error)
object UnknownBlockchain : TapError(R.string.wallet_error_unsupported_blockchain_subtitle)
object NoInternetConnection : TapError(R.string.wallet_notification_no_internet)
@ -38,11 +42,6 @@ sealed class TapError(
data class DustAmount(override val args: List<Any>) : TapError(R.string.send_error_dust_amount_format)
object DustChange : TapError(R.string.send_error_dust_change)
data class UnsupportedState(
val stateError: String,
val customMessage: String = "Unsupported state:",
) : TapError(R.string.common_custom_string, listOf("$customMessage $stateError"))
sealed class WalletManager {
object CreationError : CustomError("Can't create wallet manager")
class NoAccountError(amountToCreateAccount: String) : CustomError(amountToCreateAccount)

View file

@ -4,7 +4,6 @@ import com.tangem.common.card.FirmwareVersion
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.operations.attestation.CardVerifyAndGetInfo
@ -21,7 +20,7 @@ val CardDTO.isFirmwareMultiwalletAllowed: Boolean
get() = firmwareVersion >= FirmwareVersion.MultiWalletAvailable && settings.maxWalletsCount > 1
val CardDTO.isHdWalletAllowedByApp: Boolean
get() = settings.isHDWalletAllowed && !isSaltPay
get() = settings.isHDWalletAllowed
@Suppress("UnnecessaryParentheses")
fun CardDTO.hasSignedHashes(): Boolean {

View file

@ -1,6 +1,5 @@
package com.tangem.tap.domain.model
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.address.AddressType
import com.tangem.tap.domain.model.WalletDataModel.AddressData
import com.tangem.tap.domain.model.WalletDataModel.Status
@ -28,7 +27,6 @@ data class WalletDataModel(
val fiatRate: BigDecimal?,
val isCardSingleToken: Boolean,
val isCustom: Boolean,
val historyTransactions: List<TransactionData>?,
) {
data class WalletAddresses(
@ -79,6 +77,7 @@ data class WalletDataModel(
data class Unreachable(
override val errorMessage: String?,
override val amount: BigDecimal = BigDecimal.ZERO,
) : Status() {
override val isErrorStatus: Boolean = true
}

View file

@ -21,7 +21,6 @@ class UserWalletBuilder(
get() = when (productType) {
ProductType.Note -> "Note"
ProductType.Twins -> "Twin"
ProductType.SaltPay -> "SaltPay"
ProductType.Start2Coin -> "Start2Coin"
ProductType.Wallet -> when {
card.isBackupNotAllowed -> "Tangem card"

View file

@ -60,7 +60,6 @@ class UserWalletIdBuilder private constructor(
ProductType.Twins -> scanResponse.secondTwinPublicKey?.hexToBytes()
ProductType.Note,
ProductType.Wallet,
ProductType.SaltPay,
ProductType.Start2Coin,
-> null
},

View file

@ -9,8 +9,6 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.common.extensions.getBlockchainTxHistory
import com.tangem.tap.common.extensions.getTokenTxHistory
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
@ -114,7 +112,6 @@ private fun BlockchainNetwork.getBlockchainWalletData(
fiatRate = null,
isCardSingleToken = false,
isCustom = currency.isCustomCurrency(cardDerivationStyle),
historyTransactions = walletManager?.getBlockchainTxHistory(),
)
}
@ -138,7 +135,6 @@ private fun BlockchainNetwork.getTokensWalletsData(
fiatRate = null,
isCardSingleToken = token == primaryToken,
isCustom = currency.isCustomCurrency(cardDerivationStyle),
historyTransactions = walletManager?.getTokenTxHistory(token),
)
}
}
@ -156,7 +152,6 @@ private fun Blockchain.toBlockchainWalletData(walletManager: WalletManager): Wal
fiatRate = null,
isCardSingleToken = false,
isCustom = false,
historyTransactions = walletManager.getBlockchainTxHistory(),
)
}
@ -174,7 +169,6 @@ private fun Token.toTokenWalletData(walletManager: WalletManager, primaryToken:
fiatRate = null,
isCardSingleToken = this == primaryToken,
isCustom = false,
historyTransactions = walletManager.getTokenTxHistory(this),
)
}

View file

@ -5,8 +5,6 @@ import com.tangem.common.module.ModuleMessage
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.domain.DomainModuleMessage
import com.tangem.tap.domain.moduleMessage.domain.DomainMessageConverter
import com.tangem.tap.domain.moduleMessage.saltPay.SaltPayMessageConverter
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleMessage
class ModuleMessageConverter(
private val context: Context,
@ -15,7 +13,6 @@ class ModuleMessageConverter(
override fun convert(message: ModuleMessage): ConvertedMessage {
val convertedMessage = when (message) {
is DomainModuleMessage -> DomainMessageConverter(context).convert(message)
is SaltPayModuleMessage -> SaltPayMessageConverter(context).convert(message)
else -> null
}
return convertedMessage ?: convertUnknownMessage(message)

View file

@ -1,34 +0,0 @@
package com.tangem.tap.domain.moduleMessage.saltPay
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.tap.domain.moduleMessage.ConvertedMessage
import com.tangem.tap.domain.moduleMessage.saltPay.converter.SaltPayActivationErrorConverter
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayModuleMessage
/**
[REDACTED_AUTHOR]
*/
class SaltPayMessageConverter(
private val context: Context,
) : ModuleMessageConverter<SaltPayModuleMessage, ConvertedMessage?> {
override fun convert(message: SaltPayModuleMessage): ConvertedMessage? {
return when (message) {
is SaltPayModuleError -> SaltPayErrorConverter(context).convert(message)
else -> null
}
}
}
class SaltPayErrorConverter(
private val context: Context,
) : ModuleMessageConverter<SaltPayModuleError, ConvertedMessage?> {
override fun convert(message: SaltPayModuleError): ConvertedMessage? = when (message) {
is SaltPayActivationError -> SaltPayActivationErrorConverter(context).convert(message)
else -> null
}
}

View file

@ -1,53 +0,0 @@
package com.tangem.tap.domain.moduleMessage.saltPay.converter
import android.content.Context
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.tap.domain.moduleMessage.ConvertedDialogMessage
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
internal class SaltPayActivationErrorConverter(
private val context: Context,
) : ModuleMessageConverter<SaltPayActivationError, ConvertedDialogMessage?> {
override fun convert(message: SaltPayActivationError): ConvertedDialogMessage? {
val saltPayError = message as? SaltPayActivationError ?: throw UnsupportedOperationException()
val dialogMessage = when (saltPayError) {
SaltPayActivationError.NoGas -> {
ConvertedDialogMessage(
title = context.getString(R.string.saltpay_error_no_gas_title),
message = context.getString(R.string.saltpay_error_no_gas_message),
)
}
SaltPayActivationError.EmptyBackupCardScanned -> {
ConvertedDialogMessage(
title = context.getString(R.string.saltpay_error_empty_backup_title),
message = context.getString(R.string.saltpay_error_empty_backup_message),
)
}
SaltPayActivationError.WeakPin -> {
ConvertedDialogMessage(
title = context.getString(R.string.saltpay_error_pin_weak_title),
message = context.getString(R.string.saltpay_error_pin_weak_message),
)
}
else -> {
val errorMessage = if (saltPayError.message == SaltPayActivationError.EMPTY_MESSAGE) {
saltPayError::class.java.simpleName
} else {
saltPayError.message
}
ConvertedDialogMessage(
title = context.getString(R.string.common_error),
message = errorMessage,
)
}
}
return dialogMessage
}
}

View file

@ -0,0 +1,177 @@
package com.tangem.tap.domain.scanCard
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
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.core.analytics.AnalyticsEvent
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
internal object LegacyScanProcessor {
suspend fun scan(
cardId: String? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<ScanResponse> {
return tangemSdkManager.scanProduct(
userTokensRepository,
cardId,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
)
}
@Suppress("LongParameterList")
suspend fun scan(
analyticsEvent: AnalyticsEvent?,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
) = withMainContext {
onProgressStateChange(true)
onScanStateChange(true)
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
val result = tangemSdkManager.scanProduct(
userTokensRepository = userTokensRepository,
cardId = cardId,
)
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
result
.doOnFailure { error ->
onScanStateChange(false)
onFailure(error)
}
.doOnSuccess { scanResponse ->
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
onScanStateChange(false)
sendAnalytics(analyticsEvent, scanResponse)
showDisclaimerIfNeed(
scanResponse = scanResponse,
disclaimerWillShow = disclaimerWillShow,
onFailure = onFailure,
nextHandler = { scanResponse2 ->
onScanSuccess(
scanResponse = scanResponse2,
onProgressStateChange = onProgressStateChange,
onSuccess = onSuccess,
onWalletNotCreated = onWalletNotCreated,
)
},
)
}
}
private fun sendAnalytics(analyticsEvent: AnalyticsEvent?, scanResponse: ScanResponse) {
analyticsEvent?.let {
// this workaround needed to send CardWasScannedEvent without adding a context
val interceptor = CardContextInterceptor(scanResponse)
val params = it.params.toMutableMap()
interceptor.intercept(params)
it.params = params.toMap()
Analytics.send(it)
}
}
private suspend inline fun showDisclaimerIfNeed(
scanResponse: ScanResponse,
crossinline disclaimerWillShow: () -> Unit = {},
crossinline nextHandler: suspend (ScanResponse) -> Unit,
crossinline onFailure: suspend (error: TangemError) -> Unit,
) {
val disclaimer = scanResponse.card.createDisclaimer()
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
if (disclaimer.isAccepted()) {
nextHandler(scanResponse)
} else {
scope.launch {
delay(DELAY_SDK_DIALOG_CLOSE)
disclaimerWillShow()
store.dispatchWithMain(
DisclaimerAction.Show(
fromScreen = AppScreen.Home,
callback = DisclaimerCallback(
onAccept = {
scope.launch(Dispatchers.Main) {
nextHandler(scanResponse)
}
},
onDismiss = {
scope.launch(Dispatchers.Main) {
onFailure(TangemSdkError.UserCancelled())
}
},
),
),
)
}
}
}
@Suppress("LongMethod", "MagicNumber")
private suspend inline fun onScanSuccess(
scanResponse: ScanResponse,
crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
crossinline onWalletNotCreated: suspend () -> Unit,
crossinline onSuccess: suspend (ScanResponse) -> Unit,
) {
val globalState = store.state.globalState
val tapWalletManager = globalState.tapWalletManager
tapWalletManager.updateConfigManager(scanResponse)
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
Analytics.addContext(scanResponse)
onWalletNotCreated()
store.dispatchOnMain(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
navigateTo(appScreen) { onProgressStateChange(it) }
} else {
Analytics.setContext(scanResponse)
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
onWalletNotCreated()
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
}
}
}
private suspend inline fun navigateTo(screen: AppScreen, onProgressStateChange: (showProgress: Boolean) -> Unit) {
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
onProgressStateChange(false)
}
}

View file

@ -1,49 +1,31 @@
package com.tangem.tap.domain.scanCard
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.backup.BackupService
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
// TODO: Create repository for that
object ScanCardProcessor {
// TODO: Remove this object after feature toggle was removed and use ScanCardUseCase instead
internal object ScanCardProcessor {
private val isNewCardScanningEnabled: Boolean
get() = store.state.daggerGraphState
.get(DaggerGraphState::customTokenFeatureToggles)
.isNewCardScanningEnabled
suspend fun scan(
cardId: String? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<ScanResponse> {
return if (isNewCardScanningEnabled) {
UseCaseScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository)
} else {
LegacyScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository)
}
}
@Suppress("LongParameterList")
suspend fun scan(
analyticsEvent: AnalyticsEvent? = null,
cardId: String? = null,
@ -53,212 +35,29 @@ object ScanCardProcessor {
disclaimerWillShow: () -> Unit = {},
onFailure: suspend (error: TangemError) -> Unit = {},
onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {},
) = withMainContext {
onProgressStateChange(true)
onScanStateChange(true)
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
val result = tangemSdkManager.scanProduct(
userTokensRepository = userTokensRepository,
cardId = cardId,
)
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
result
.doOnFailure { error ->
onScanStateChange(false)
onFailure(error)
}
.doOnSuccess { scanResponse ->
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
onScanStateChange(false)
sendAnalytics(analyticsEvent, scanResponse)
checkForUnfinishedBackupForSaltPay(
backupService = backupService,
scanResponse = scanResponse,
onFailure = onFailure,
nextHandler = { scanResponse1 ->
showDisclaimerIfNeed(
scanResponse = scanResponse1,
disclaimerWillShow = disclaimerWillShow,
onFailure = onFailure,
nextHandler = { scanResponse2 ->
onScanSuccess(
scanResponse = scanResponse2,
onProgressStateChange = onProgressStateChange,
onSuccess = onSuccess,
onWalletNotCreated = onWalletNotCreated,
onFailure = onFailure,
)
},
)
},
)
}
}
private fun sendAnalytics(analyticsEvent: AnalyticsEvent?, scanResponse: ScanResponse) {
analyticsEvent?.let {
// this workaround needed to send CardWasScannedEvent without adding a context
val interceptor = CardContextInterceptor(scanResponse)
val params = it.params.toMutableMap()
interceptor.intercept(params)
it.params = params.toMap()
Analytics.send(it)
}
}
/**
* It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards
* see BackupAction.CheckForUnfinishedBackup
* If user touches card other than Visa SaltPay - show dialog and block next processing
*/
private suspend inline fun checkForUnfinishedBackupForSaltPay(
backupService: BackupService,
scanResponse: ScanResponse,
nextHandler: (ScanResponse) -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
) {
if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
nextHandler(scanResponse)
return
}
val isTheSamePrimaryCard = backupService.primaryCardId
?.let { it == scanResponse.card.cardId }
?: false
if (scanResponse.cardTypesResolver.isSaltPayWallet() || !isTheSamePrimaryCard) {
val error = SaltPayActivationError.PutVisaCard
SaltPayExceptionHandler.handle(error)
onFailure(TangemSdkError.ExceptionError(error))
if (isNewCardScanningEnabled) {
UseCaseScanProcessor.scan(
analyticsEvent,
cardId,
onProgressStateChange,
onScanStateChange,
onWalletNotCreated,
disclaimerWillShow,
onFailure,
onSuccess,
)
} else {
nextHandler(scanResponse)
LegacyScanProcessor.scan(
analyticsEvent,
cardId,
onProgressStateChange,
onScanStateChange,
onWalletNotCreated,
disclaimerWillShow,
onFailure,
onSuccess,
)
}
}
private suspend inline fun showDisclaimerIfNeed(
scanResponse: ScanResponse,
crossinline disclaimerWillShow: () -> Unit = {},
crossinline nextHandler: suspend (ScanResponse) -> Unit,
crossinline onFailure: suspend (error: TangemError) -> Unit,
) {
val disclaimer = scanResponse.card.createDisclaimer()
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
if (disclaimer.isAccepted()) {
nextHandler(scanResponse)
} else {
scope.launch {
delay(DELAY_SDK_DIALOG_CLOSE)
disclaimerWillShow()
dispatchOnMain(
DisclaimerAction.Show(
fromScreen = AppScreen.Home,
callback = DisclaimerCallback(
onAccept = {
scope.launch(Dispatchers.Main) {
nextHandler(scanResponse)
}
},
onDismiss = {
scope.launch(Dispatchers.Main) {
onFailure(TangemSdkError.UserCancelled())
}
},
),
),
)
}
}
}
@Suppress("LongMethod", "MagicNumber")
private suspend inline fun onScanSuccess(
scanResponse: ScanResponse,
crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
crossinline onWalletNotCreated: suspend () -> Unit,
crossinline onSuccess: suspend (ScanResponse) -> Unit,
crossinline onFailure: suspend (error: TangemError) -> Unit,
) {
val globalState = store.state.globalState
val tapWalletManager = globalState.tapWalletManager
tapWalletManager.updateConfigManager(scanResponse)
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
val cardTypesResolver = scanResponse.cardTypesResolver
if (cardTypesResolver.isSaltPay()) {
if (cardTypesResolver.isSaltPayVisa()) {
val manager = SaltPayActivationManagerFactory(
blockchain = scanResponse.cardTypesResolver.getBlockchain(),
card = scanResponse.card,
).create()
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager)
delay(500)
withMainContext {
when (result) {
is Result.Success -> {
Analytics.addContext(scanResponse)
val isOnboardingCase = result.data
if (isOnboardingCase) {
onWalletNotCreated()
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager))
store.dispatch(OnboardingSaltPayAction.Update(withAnalytics = false))
navigateTo(AppScreen.OnboardingWallet) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
}
}
is Result.Failure -> {
delay(DELAY_SDK_DIALOG_CLOSE)
SaltPayExceptionHandler.handle(result.error)
onFailure(TangemSdkError.ExceptionError(result.error))
}
}
}
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
if (scanResponse.card.backupStatus?.isActive == false) {
val error = SaltPayActivationError.PutVisaCard
SaltPayExceptionHandler.handle(error)
onFailure(TangemSdkError.ExceptionError(error))
} else {
Analytics.setContext(scanResponse)
onSuccess(scanResponse)
}
}
} else {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
Analytics.addContext(scanResponse)
onWalletNotCreated()
store.dispatchOnMain(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
navigateTo(appScreen) { onProgressStateChange(it) }
} else {
Analytics.setContext(scanResponse)
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
onWalletNotCreated()
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
}
}
}
}
private suspend inline fun navigateTo(screen: AppScreen, onProgressStateChange: (showProgress: Boolean) -> Unit) {
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
onProgressStateChange(false)
}
}

View file

@ -0,0 +1,113 @@
package com.tangem.tap.domain.scanCard
import arrow.fx.coroutines.resourceScope
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.scanCard.chains.*
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import kotlinx.coroutines.delay
internal object UseCaseScanProcessor {
private val scanCardExceptionConverter = ScanCardExceptionConverter()
suspend fun scan(
cardId: String? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<ScanResponse> {
val scanCardUseCase = store.state.daggerGraphState.get(DaggerGraphState::scanCardUseCase)
return scanCardUseCase(cardId, allowsRequestAccessCodeFromRepository)
.fold(
ifLeft = { CompletionResult.Failure(scanCardExceptionConverter.convertBack(it)) },
ifRight = { CompletionResult.Success(it) },
)
}
@Suppress("LongParameterList")
suspend fun scan(
analyticsEvent: AnalyticsEvent?,
cardId: String?,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit,
onWalletNotCreated: suspend () -> Unit,
disclaimerWillShow: () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
) = progressScope(onProgressStateChange) {
onScanStateChange(true)
val scanCardUseCase = store.state.daggerGraphState.get(DaggerGraphState::scanCardUseCase)
val chains = buildList {
add(ScanningFinishedChain { onScanStateChange(false) })
if (analyticsEvent != null) {
add(AnalyticsChain(analyticsEvent))
}
add(DisclaimerChain(store, disclaimerWillShow))
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager, preferencesStorage))
}
scanCardUseCase(cardId, afterScanChains = chains)
.map { onSuccess(it) }
.mapLeft { proceedWithException(it, onWalletNotCreated, onFailure) }
}
private suspend fun proceedWithException(
exception: ScanCardException,
onWalletNotCreated: suspend () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
) {
when (exception) {
is ScanCardException.ChainException -> proceedWithScanChainException(
exception,
onWalletNotCreated,
onFailure,
)
is ScanCardException.UnknownException,
is ScanCardException.UserCancelled,
is ScanCardException.WrongAccessCode,
is ScanCardException.WrongCardId,
-> onFailure(scanCardExceptionConverter.convertBack(exception))
}
}
private suspend fun proceedWithScanChainException(
exception: ScanCardException.ChainException,
onWalletNotCreated: suspend () -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
) {
when (exception) {
is ScanChainException.OnboardingNeeded -> {
navigateTo(exception.onboardingRoute)
onWalletNotCreated()
}
is ScanChainException.DisclaimerWasCanceled,
-> onFailure(scanCardExceptionConverter.convertBack(exception))
}
}
private suspend fun progressScope(
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
action: suspend () -> Unit,
) = resourceScope {
install(
acquire = { onProgressStateChange(true) },
release = { _, _ -> onProgressStateChange(false) },
)
action()
}
private suspend inline fun navigateTo(screen: AppScreen) {
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
}
}

View file

@ -3,12 +3,9 @@ package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
@ -21,13 +18,8 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import kotlinx.coroutines.delay
import org.rekotlin.Store
@ -35,9 +27,7 @@ import org.rekotlin.Store
* Handles the verification process to determine if the scanned card requires onboarding.
*
* Returns:
* - [ScanChainException.OnboardingNeeded] if onboarding required for none SaltPay cards.
* - [ScanChainException.CheckForSaltPayOnboardingCaseException] if unable to check for SaltPay onboarding case.
* - [ScanChainException.PutSaltPayVisaCard] if scanned SaltPay card is not activated and onboarding required.
* - [ScanChainException.OnboardingNeeded] if onboarding required.
*
* @param store the [Store] that holds the state of the app.
* @param tapWalletManager manager responsible for handling operations related to the Wallet.
@ -54,54 +44,10 @@ class CheckForOnboardingChain(
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
val cardTypesResolver = previousChainResult.cardTypesResolver
tapWalletManager.updateConfigManager(previousChainResult)
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult))
return when {
cardTypesResolver.isSaltPayVisa() -> {
val manager = SaltPayActivationManagerFactory(
blockchain = previousChainResult.cardTypesResolver.getBlockchain(),
card = previousChainResult.card,
).create()
val result = OnboardingSaltPayHelper.isOnboardingCase(previousChainResult, manager)
delay(timeMillis = 500)
withMainContext {
when (result) {
is Result.Success -> {
Analytics.addContext(previousChainResult)
val isOnboardingCase = result.data
if (isOnboardingCase) {
store.dispatch(
GlobalAction.Onboarding.Start(previousChainResult, canSkipBackup = false),
)
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager))
store.dispatch(OnboardingSaltPayAction.Update(withAnalytics = false))
ScanChainException.OnboardingNeeded(AppScreen.OnboardingWallet).left()
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
previousChainResult.right()
}
}
is Result.Failure -> {
delay(DELAY_SDK_DIALOG_CLOSE)
SaltPayExceptionHandler.handle(result.error)
ScanChainException.CheckForSaltPayOnboardingCaseException(result.error).left()
}
}
}
}
cardTypesResolver.isSaltPayWallet() -> {
delay(DELAY_SDK_DIALOG_CLOSE)
if (previousChainResult.card.backupStatus?.isActive == false) {
SaltPayExceptionHandler.handle(SaltPayActivationError.PutVisaCard)
ScanChainException.PutSaltPayVisaCard.left()
} else {
Analytics.setContext(previousChainResult)
previousChainResult.right()
}
}
OnboardingHelper.isOnboardingCase(previousChainResult) -> {
Analytics.addContext(previousChainResult)
store.dispatchOnMain(GlobalAction.Onboarding.Start(previousChainResult, canSkipBackup = true))

View file

@ -1,44 +0,0 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.backup.BackupService
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
/**
* Handles the verification for unfinished SaltPay backup processes after the card scanning operation.
*
* Returns [ScanChainException.PutSaltPayVisaCard] if backup is not finished.
*
* @param backupService a service responsible for managing backup operations, used here to check for unfinished backups
* and verify the primary card type.
*
* @see Chain for more information about the Chain interface.
*/
internal class CheckForUnfinishedSaltPayBackupChain(
private val backupService: BackupService,
) : Chain<ScanCardException.ChainException, ScanResponse> {
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
return previousChainResult.right()
}
val isTheSamePrimaryCard = backupService.primaryCardId
?.let { it == previousChainResult.card.cardId }
?: false
return if (previousChainResult.cardTypesResolver.isSaltPayWallet() || !isTheSamePrimaryCard) {
ScanChainException.PutSaltPayVisaCard.left()
} else {
previousChainResult.right()
}
}
}

View file

@ -5,11 +5,6 @@ import com.tangem.tap.common.redux.navigation.AppScreen
sealed class ScanChainException : ScanCardException.ChainException() {
/**
* May be returned from [CheckForOnboardingChain] and [CheckForUnfinishedSaltPayBackupChain]
* */
object PutSaltPayVisaCard : ScanChainException()
/**
* May be returned from [DisclaimerChain]
* */
@ -21,11 +16,4 @@ sealed class ScanChainException : ScanCardException.ChainException() {
* @param onboardingRoute route where to navigate
* */
data class OnboardingNeeded(val onboardingRoute: AppScreen) : ScanChainException()
/**
* May be returned from [CheckForOnboardingChain]
*
* @param cause cause of exception
* */
data class CheckForSaltPayOnboardingCaseException(override val cause: Throwable?) : ScanChainException()
}

View file

@ -0,0 +1,38 @@
package com.tangem.tap.domain.scanCard.repository
import arrow.core.Either
import arrow.core.raise.either
import com.tangem.common.CompletionResult
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.card.repository.ScanCardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
import com.tangem.tap.domain.tokens.UserTokensRepository
internal class DefaultScanCardRepository(
// FIXME: The repository should not depend on another repository.
// But now we need to provide loadBlockchainsToDerive() to ScanProductTask and it's hard to move this method from
// UserTokensRepository.
private val userTokensRepository: UserTokensRepository,
private val tangemSdkManager: TangemSdkManager,
) : ScanCardRepository {
private val exceptionConverter = ScanCardExceptionConverter()
override suspend fun scanCard(
cardId: String?,
allowRequestAccessCodeFromStorage: Boolean,
): Either<ScanCardException, ScanResponse> = either {
when (
val result = tangemSdkManager.scanProduct(
cardId = cardId,
userTokensRepository = userTokensRepository,
allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage,
)
) {
is CompletionResult.Success -> result.data
is CompletionResult.Failure -> raise(exceptionConverter.convert(result.error))
}
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.tap.domain.scanCard.utils
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.card.ScanCardException
import com.tangem.tap.domain.scanCard.chains.ScanChainException
import com.tangem.utils.converter.TwoWayConverter
internal class ScanCardExceptionConverter : TwoWayConverter<TangemError, ScanCardException> {
override fun convert(value: TangemError): ScanCardException = when (value) {
is TangemSdkError -> convertTangemSdkError(value)
else -> ScanCardException.UnknownException(value)
}
override fun convertBack(value: ScanCardException): TangemError {
return when (value) {
is ScanCardException.UnknownException -> TangemSdkError.ExceptionError(value)
is ScanCardException.UserCancelled -> TangemSdkError.UserCancelled()
is ScanCardException.WrongAccessCode -> TangemSdkError.WrongAccessCode()
is ScanCardException.WrongCardId -> TangemSdkError.WrongCardNumber(value.cardId)
is ScanCardException.ChainException -> concertScanChainException(value)
}
}
private fun convertTangemSdkError(value: TangemSdkError): ScanCardException = when (value) {
is TangemSdkError.UserCancelled -> ScanCardException.UserCancelled
is TangemSdkError.WrongAccessCode,
is TangemSdkError.WrongPasscode,
-> ScanCardException.WrongAccessCode
is TangemSdkError.WrongCardNumber -> ScanCardException.WrongCardId(value.cardId)
else -> ScanCardException.UnknownException(value)
}
private fun concertScanChainException(value: ScanCardException.ChainException): TangemError {
return when (val e = value as? ScanChainException) {
is ScanChainException.DisclaimerWasCanceled -> TangemSdkError.UserCancelled()
is ScanChainException.OnboardingNeeded,
null,
-> TangemSdkError.ExceptionError(e?.cause)
}
}
}

View file

@ -20,7 +20,6 @@ import com.tangem.crypto.CryptoUtils
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.TapWorkarounds.isExcluded
import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
@ -189,9 +188,7 @@ private class ScanWalletProcessor(
val activationInProgress = preferencesStorage.usedCardsPrefStorage.isActivationInProgress(card.cardId)
@Suppress("ComplexCondition")
if (card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty() &&
(activationInProgress || card.isSaltPay)
) {
if (card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty() && activationInProgress) {
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
when (linkingResult) {
is CompletionResult.Success -> {
@ -213,10 +210,7 @@ private class ScanWalletProcessor(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val productType = when (card.isSaltPay) {
true -> ProductType.SaltPay
else -> ProductType.Wallet
}
val productType = ProductType.Wallet
scope.launch {
val derivations = collectDerivations(card)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {

View file

@ -47,6 +47,7 @@ class UserTokensRepository(
storageService.saveUserTokens(userWalletId = userWalletId, tokens = userTokens)
}
// FIXME: Move to data layer
suspend fun loadBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> = withContext(dispatchers.io) {
val userWalletId = getUserWalletId(card) ?: return@withContext emptyList()
val blockchainNetworks = loadTokensOffline(userWalletId = userWalletId).toBlockchainNetworks()
@ -62,6 +63,7 @@ class UserTokensRepository(
return storageService.getUserTokens(userWalletId = userWalletId) ?: emptyList()
}
// FIXME: Move to user wallet config
private fun loadDemoCurrencies(): List<Currency> {
return DemoHelper.config.demoBlockchains
.map { blockchain ->

View file

@ -2,7 +2,6 @@ package com.tangem.tap.domain.userWalletList
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.TwinsHelper
import com.tangem.operations.attestation.OnlineCardVerifier
@ -43,7 +42,6 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
SaltPayWorkaround.isSaltPayCardId(cardId) -> Artwork.SALT_PAY_URL
else -> when (TwinsHelper.getTwinCardNumber(cardId)) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2

View file

@ -2,9 +2,7 @@ package com.tangem.tap.domain.walletStores.repository.implementation
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.TransactionHistoryProvider
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result.Failure
@ -35,7 +33,6 @@ import com.tangem.tap.domain.walletStores.repository.implementation.utils.update
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithFiatRates
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithMissedDerivation
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithRent
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithTxHistories
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithUnreachable
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
@ -248,11 +245,6 @@ internal class DefaultWalletAmountsRepository(
TestActions.testAmountInjectionForWalletManagerEnabled = false
} else {
walletManager.update()
val wallet = walletManager.wallet
if (wallet.blockchain == Blockchain.SaltPay && walletManager is TransactionHistoryProvider) {
walletManager.getTransactionHistory(wallet.address, wallet.blockchain, wallet.getTokens())
}
}
}
@ -349,7 +341,7 @@ internal class DefaultWalletAmountsRepository(
it.updateWithDemoAmounts(wallet = updatedWallet)
} else {
it.updateWithAmounts(wallet = updatedWallet)
}.updateWithTxHistories(wallet = updatedWallet)
}
},
)
}

View file

@ -5,8 +5,6 @@ import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.core.TangemError
import com.tangem.tap.common.extensions.getBlockchainTxHistory
import com.tangem.tap.common.extensions.getTokenTxHistory
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.model.WalletDataModel
@ -28,21 +26,6 @@ internal fun List<WalletDataModel>.updateWithFiatRates(fiatRates: Map<String, Do
}
}
internal fun WalletDataModel.updateWithTxHistory(wallet: Wallet): WalletDataModel {
return this.copy(
historyTransactions = when (currency) {
is Currency.Blockchain -> wallet.getBlockchainTxHistory()
is Currency.Token -> wallet.getTokenTxHistory(currency.token)
},
)
}
internal fun List<WalletDataModel>.updateWithTxHistories(wallet: Wallet): List<WalletDataModel> {
return this.map { walletData ->
walletData.updateWithTxHistory(wallet)
}
}
internal fun List<WalletDataModel>.updateWithAmounts(wallet: Wallet): List<WalletDataModel> {
return this.map { walletData ->
walletData.updateWithAmount(wallet)
@ -170,7 +153,10 @@ internal fun List<WalletDataModel>.updateWithMissedDerivation(): List<WalletData
internal fun List<WalletDataModel>.updateWithUnreachable(): List<WalletDataModel> {
return this.map { walletData ->
walletData.copy(
status = WalletDataModel.Unreachable(errorMessage = null),
status = WalletDataModel.Unreachable(
errorMessage = null,
amount = walletData.status.amount,
),
)
}
}

View file

@ -40,12 +40,6 @@ internal fun WalletStoreModel.updateWithError(wallet: Wallet, error: TangemError
)
}
internal fun WalletStoreModel.updateWithTxHistories(wallet: Wallet): WalletStoreModel {
return this.copy(
walletsData = walletsData.updateWithTxHistories(wallet = wallet),
)
}
internal fun WalletStoreModel.updateWithAmounts(wallet: Wallet): WalletStoreModel {
return this.copy(
walletsData = walletsData.updateWithAmounts(wallet = wallet),

View file

@ -9,4 +9,6 @@ interface CustomTokenFeatureToggles {
/** Availability of redesigned screen (internal feature) */
val isRedesignedScreenEnabled: Boolean
val isNewCardScanningEnabled: Boolean
}

View file

@ -16,4 +16,7 @@ internal class DefaultCustomTokenFeatureToggles(
override val isRedesignedScreenEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED")
override val isNewCardScanningEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED")
}

View file

@ -67,6 +67,8 @@ internal data class AddCustomTokenChooseTokenBottomSheet(
* @property tokenSymbolInputField input field to enter the token symbol
* @property decimalsInputField input field to enter the token decimals
* @property derivationPathSelectorField selector field to select the derivation path
* @property derivationPathInputField input field for a custom derivation path
* @property showTokenFields if token fields should be shown
*/
internal data class AddCustomTokenForm(
val contractAddressInputField: AddCustomTokenInputField.ContactAddress,
@ -75,6 +77,8 @@ internal data class AddCustomTokenForm(
val tokenSymbolInputField: AddCustomTokenInputField.TokenSymbol,
val decimalsInputField: AddCustomTokenInputField.Decimals,
val derivationPathSelectorField: AddCustomTokenSelectorField.DerivationPath?,
val derivationPathInputField: AddCustomTokenInputField.DerivationPath?,
val showTokenFields: Boolean = false,
)
/** Base input field model of add custom token screen */
@ -174,6 +178,25 @@ internal sealed interface AddCustomTokenInputField {
override val placeholder: TextReference,
val isEnabled: Boolean,
) : AddCustomTokenInputField
/**
* Input field model to enter a custom derivation path
*
* @property value current value
* @property onValueChange lambda be invoked when value is been changed
* @property keyboardOptions keyboard options
* @property label label
* @property placeholder placeholder (hint)
* @property showField whether the field should be shown
*/
data class DerivationPath(
override val value: String,
override val onValueChange: (String) -> Unit,
override val keyboardOptions: KeyboardOptions,
override val label: TextReference,
override val placeholder: TextReference,
val showField: Boolean = false,
) : AddCustomTokenInputField
}
/** Base selector field model of add custom token screen */
@ -251,10 +274,15 @@ internal sealed interface AddCustomTokenSelectorField {
override val title: TextReference,
override val blockchain: Blockchain,
val subtitle: TextReference,
val type: DerivationPathSelectorType = DerivationPathSelectorType.BLOCKCHAIN,
) : SelectorItem
}
}
enum class DerivationPathSelectorType {
DEFAULT, CUSTOM, BLOCKCHAIN
}
/**
* Warning model of add custom token screen
*
@ -276,6 +304,10 @@ internal sealed class AddCustomTokenWarning(val description: TextReference) {
object UnsupportedSolanaToken : AddCustomTokenWarning(
description = TextReference.Res(R.string.alert_manage_tokens_unsupported_message),
)
object WrongDerivationPath : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_invalid_derivation_path),
)
}
/**

View file

@ -4,14 +4,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenWarning
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
import com.tangem.tap.features.customtoken.impl.presentation.models.*
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
@ -85,6 +78,7 @@ internal object AddCustomTokenPreviewData {
onMenuItemClick = {},
isEnabled = true,
),
derivationPathInputField = null,
)
}

View file

@ -5,12 +5,11 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.PrimaryButtonIconLeft
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
import com.tangem.wallet.R
@ -25,13 +24,13 @@ import com.tangem.wallet.R
*/
@Composable
internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, modifier: Modifier = Modifier) {
PrimaryButtonIconLeft(
PrimaryButtonIconStart(
modifier = modifier
.imePadding()
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(id = R.string.common_add),
icon = painterResource(id = R.drawable.ic_plus_24),
iconResId = R.drawable.ic_plus_24,
enabled = model.isEnabled,
onClick = model.onClick,
)

View file

@ -1,31 +1,11 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExposedDropdownMenuBox
import androidx.compose.material.ExposedDropdownMenuDefaults
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@ -62,12 +42,13 @@ internal fun AddCustomTokenForm(model: AddCustomTokenForm) {
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
InputField(model = model.contractAddressInputField)
if (model.showTokenFields) InputField(model = model.contractAddressInputField)
SelectorField(model = model.networkSelectorField)
InputField(model = model.tokenNameInputField)
InputField(model = model.tokenSymbolInputField)
InputField(model = model.decimalsInputField)
if (model.showTokenFields) InputField(model = model.tokenNameInputField)
if (model.showTokenFields) InputField(model = model.tokenSymbolInputField)
if (model.showTokenFields) InputField(model = model.decimalsInputField)
model.derivationPathSelectorField?.let { SelectorField(model = it) }
if (model.derivationPathInputField?.showField == true) InputField(model = model.derivationPathInputField)
}
}
}
@ -103,6 +84,7 @@ private fun TextField(model: AddCustomTokenInputField, isError: Boolean) {
is AddCustomTokenInputField.Decimals -> model.isEnabled
is AddCustomTokenInputField.TokenName -> model.isEnabled
is AddCustomTokenInputField.TokenSymbol -> model.isEnabled
is AddCustomTokenInputField.DerivationPath -> true
}
OutlinedTextField(

View file

@ -15,6 +15,7 @@ import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.HDWalletError
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.*
@ -135,18 +136,28 @@ internal class AddCustomTokenViewModel @Inject constructor(
tokenSymbolInputField = createTokenSymbolInputField(),
decimalsInputField = createDecimalsInputField(),
derivationPathSelectorField = createDerivationPathsSelectorField(),
derivationPathInputField = createDerivationPathInputField(),
)
}
fun createDerivationPathSelectorItem(blockchain: Blockchain): SelectorItem.TitleWithSubtitle {
return if (blockchain == Blockchain.Unknown) {
SelectorItem.TitleWithSubtitle(
fun createDerivationPathSelectorAdditionalItem(
blockchain: Blockchain,
type: DerivationPathSelectorType = DerivationPathSelectorType.BLOCKCHAIN,
): SelectorItem.TitleWithSubtitle {
return when (type) {
DerivationPathSelectorType.DEFAULT -> SelectorItem.TitleWithSubtitle(
title = TextReference.Res(R.string.custom_token_derivation_path_default),
subtitle = TextReference.Res(R.string.custom_token_derivation_path_default),
blockchain = Blockchain.Unknown,
type = DerivationPathSelectorType.DEFAULT,
)
} else {
SelectorItem.TitleWithSubtitle(
DerivationPathSelectorType.CUSTOM -> SelectorItem.TitleWithSubtitle(
title = TextReference.Res(R.string.custom_token_custom_derivation),
subtitle = TextReference.Res(R.string.custom_token_custom_derivation),
blockchain = Blockchain.Unknown,
type = DerivationPathSelectorType.CUSTOM,
)
DerivationPathSelectorType.BLOCKCHAIN -> SelectorItem.TitleWithSubtitle(
title = blockchain.derivationPath(DerivationStyle.LEGACY)?.rawPath?.let(TextReference::Str)
?: TextReference.Res(R.string.custom_token_derivation_path_default),
subtitle = TextReference.Str(blockchain.fullName),
@ -175,7 +186,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
onValueChange = actionsHandler::onContactAddressValueChange,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_contract_address_input_title),
placeholder = TextReference.Str(value = "0x0000000000000000000000000000000000000000"),
placeholder = TextReference.Str(value = CONTRACT_ADDRESS_PLACEHOLDER),
isLoading = false,
isError = false,
error = null,
@ -196,8 +207,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
val defaultNetwork = createNetworkSelectorItem(blockchain = Blockchain.Unknown)
return listOf(defaultNetwork) + Blockchain.values()
.filter { blockchain ->
(blockchain.isEvm() || blockchain.canHandleTokens()) &&
reduxStateHolder.scanResponse?.card?.supportedBlockchains()?.contains(blockchain) == true
reduxStateHolder.scanResponse?.card?.supportedBlockchains()?.contains(blockchain) == true
}
.sortedBy(Blockchain::fullName)
.map(::createNetworkSelectorItem)
@ -231,7 +241,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
onValueChange = actionsHandler::onDecimalsValueChange,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_decimals_input_title),
placeholder = TextReference.Str(value = "8"),
placeholder = TextReference.Str(value = DECIMALS_PLACEHOLDER),
isEnabled = false,
)
}
@ -250,11 +260,31 @@ internal class AddCustomTokenViewModel @Inject constructor(
}
private fun getDerivationPathsSelectorItems(): List<SelectorItem.TitleWithSubtitle> {
val defaultDerivationPath = createDerivationPathSelectorItem(Blockchain.Unknown)
return listOf(defaultDerivationPath) + Blockchain.values()
.filter { blockchain -> blockchain.isEvm() && blockchain.isSupportedInApp() && !blockchain.isTestnet() }
return listOf(
createDerivationPathSelectorAdditionalItem(
blockchain = Blockchain.Unknown,
type = DerivationPathSelectorType.DEFAULT,
),
createDerivationPathSelectorAdditionalItem(
blockchain = Blockchain.Unknown,
type = DerivationPathSelectorType.CUSTOM,
),
) + Blockchain.values()
.filter { blockchain -> blockchain.isSupportedInApp() && !blockchain.isTestnet() }
.sortedBy(Blockchain::fullName)
.map(::createDerivationPathSelectorItem)
.map(::createDerivationPathSelectorAdditionalItem)
}
private fun createDerivationPathInputField(): AddCustomTokenInputField.DerivationPath? {
if (reduxStateHolder.scanResponse?.card?.settings?.isHDWalletAllowed == false) return null
return AddCustomTokenInputField.DerivationPath(
value = "",
onValueChange = actionsHandler::onDerivationPathValueChange,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
label = TextReference.Res(R.string.custom_token_custom_derivation),
placeholder = TextReference.Str(value = DERIVATION_PATH_PLACEHOLDER),
)
}
}
@ -306,25 +336,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
Timber.e(it)
}
updateDerivationPathSelector()
updateWarnings()
updateFloatingButton()
}
}
private fun updateDerivationPathSelector() {
uiState = uiState.copySealed(
form = uiState.form.copy(
derivationPathSelectorField = uiState.form.derivationPathSelectorField?.copy(
isEnabled = uiState.form.networkSelectorField.selectedItem.blockchain.isEvm(),
),
),
)
}
private fun isDerivationPathSelected(): Boolean {
return with(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) {
this != null && this != Blockchain.Unknown
this != null && this != Blockchain.Unknown ||
uiState.form.derivationPathSelectorField?.selectedItem?.type == DerivationPathSelectorType.CUSTOM &&
!uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
}
}
@ -397,6 +418,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
}
private fun updateFloatingButton() {
uiState = updateStateWithDerivationError(uiState)
if (isCustomTokenAlreadyAdded()) {
uiState = uiState.copySealed(
warnings = uiState.warnings + AddCustomTokenWarning.TokenAlreadyAdded,
@ -405,6 +427,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
return
}
val isCorrectDerivationInput = !uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
val state = when {
isAllTokenFieldsFilled() && isNetworkSelected() -> {
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
@ -418,7 +441,8 @@ internal class AddCustomTokenViewModel @Inject constructor(
uiState.copySealed(
floatingButton = uiState.floatingButton.copy(
isEnabled = error is ContractAddressValidatorResult.Success && isSupportedToken,
isEnabled = error is ContractAddressValidatorResult.Success &&
isSupportedToken && isCorrectDerivationInput,
),
)
}
@ -430,17 +454,43 @@ internal class AddCustomTokenViewModel @Inject constructor(
else -> {
uiState.copySealed(
floatingButton = uiState.floatingButton.copy(
isEnabled = if (isNetworkSelected()) !isBlockchainAlreadyAdded() else false,
isEnabled = if (isNetworkSelected()) {
!isBlockchainAlreadyAdded() && isCorrectDerivationInput
} else {
false
},
),
)
}
}
uiState = state.copySealed(
warnings = uiState.warnings - AddCustomTokenWarning.TokenAlreadyAdded,
warnings = state.warnings - AddCustomTokenWarning.TokenAlreadyAdded,
)
}
private fun updateStateWithDerivationError(uiState: AddCustomTokenStateHolder): AddCustomTokenStateHolder {
val updatedWarnings = if (isWrongDerivationPathEntered(
derivationPathSelectorType = uiState.form.derivationPathSelectorField?.selectedItem?.type,
derivationPath = getDerivationPath(),
)
) {
uiState.warnings + AddCustomTokenWarning.WrongDerivationPath
} else {
uiState.warnings - AddCustomTokenWarning.WrongDerivationPath
}
return uiState.copySealed(
warnings = updatedWarnings,
)
}
private fun isWrongDerivationPathEntered(
derivationPathSelectorType: DerivationPathSelectorType?,
derivationPath: DerivationPath?,
): Boolean {
return derivationPathSelectorType == DerivationPathSelectorType.CUSTOM && derivationPath == null
}
private fun isCustomTokenAlreadyAdded(): Boolean {
return when (getCustomTokenType()) {
CustomTokenType.TOKEN -> isTokenAlreadyAdded()
@ -520,16 +570,36 @@ internal class AddCustomTokenViewModel @Inject constructor(
}
private fun getDerivationPath(): DerivationPath? {
return when (uiState.form.derivationPathSelectorField?.selectedItem?.type) {
DerivationPathSelectorType.CUSTOM ->
createDerivationPathOrNull(uiState.form.derivationPathInputField?.value ?: "")
else ->
getDerivationPathForBlockchain(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain)
}
}
private fun createDerivationPathOrNull(rawPath: String): DerivationPath? {
return try {
DerivationPath(rawPath)
} catch (error: HDWalletError) {
null
}
}
private fun getDerivationPathForBlockchain(blockchain: Blockchain?): DerivationPath? {
if (blockchain == null) return null
val derivationStyle = if (!isDerivationPathSelected()) {
reduxStateHolder.scanResponse?.card?.derivationStyle
} else {
DerivationStyle.LEGACY
}
return when (val derivationNetwork = uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) {
Blockchain.Unknown -> uiState.form.networkSelectorField.selectedItem.blockchain
else -> derivationNetwork
}?.derivationPath(derivationStyle)
val derivationNetwork = if (blockchain == Blockchain.Unknown) {
uiState.form.networkSelectorField.selectedItem.blockchain
} else {
blockchain
}
return derivationNetwork.derivationPath(derivationStyle)
}
private inner class ActionsHandler(private val featureRouter: CustomTokenRouter) {
@ -566,7 +636,6 @@ internal class AddCustomTokenViewModel @Inject constructor(
is ContractAddressValidatorResult.Error -> {
handleContractAddressErrorValidation(type = validatorResult.type)
updateDerivationPathSelector()
updateWarnings()
updateFloatingButton()
}
@ -574,11 +643,13 @@ internal class AddCustomTokenViewModel @Inject constructor(
}
fun onNetworkSelectorItemClick(index: Int) {
val selectedItem = requireNotNull(uiState.form.networkSelectorField.items.getOrNull(index))
uiState = uiState.copySealed(
form = uiState.form.copy(
networkSelectorField = uiState.form.networkSelectorField.copy(
selectedItem = requireNotNull(uiState.form.networkSelectorField.items.getOrNull(index)),
selectedItem = selectedItem,
),
showTokenFields = selectedItem.blockchain.canHandleTokens(),
),
)
onContactAddressValueChange(uiState.form.contractAddressInputField.value)
@ -612,12 +683,26 @@ internal class AddCustomTokenViewModel @Inject constructor(
}
fun onDerivationPathSelectorItemClick(index: Int) {
val field = requireNotNull(uiState.form.derivationPathSelectorField)
val derivationSelector = requireNotNull(uiState.form.derivationPathSelectorField)
val selected = requireNotNull(derivationSelector.items.getOrNull(index))
val derivationInputField = requireNotNull(uiState.form.derivationPathInputField)
uiState = uiState.copySealed(
form = uiState.form.copy(
derivationPathSelectorField = field.copy(
selectedItem = requireNotNull(field.items.getOrNull(index)),
derivationPathSelectorField = derivationSelector.copy(
selectedItem = selected,
),
derivationPathInputField = derivationInputField.copy(
showField = selected.type == DerivationPathSelectorType.CUSTOM,
),
),
)
updateFloatingButton()
}
fun onDerivationPathValueChange(enteredValue: String) {
uiState = uiState.copySealed(
form = uiState.form.copy(
derivationPathInputField = uiState.form.derivationPathInputField?.copy(value = enteredValue),
),
)
updateFloatingButton()
@ -691,7 +776,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false),
derivationPathSelectorField = derivationPathSelectorField?.copy(
isEnabled = true,
selectedItem = formStateBuilder.createDerivationPathSelectorItem(Blockchain.Unknown),
selectedItem = formStateBuilder
.createDerivationPathSelectorAdditionalItem(
blockchain = Blockchain.Unknown,
type = DerivationPathSelectorType.DEFAULT,
),
),
),
)
@ -722,5 +811,8 @@ internal class AddCustomTokenViewModel @Inject constructor(
TestTokenItem(name = "USDT (invalid - 1/3 of address)", address = "Es9vMFrzaCERmJ"),
TestTokenItem(name = "ETH (full)", address = "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk"),
)
const val DERIVATION_PATH_PLACEHOLDER = "m/44'/0'/0'/0/0"
const val DECIMALS_PLACEHOLDER = "8"
const val CONTRACT_ADDRESS_PLACEHOLDER = "0x0000000000000000000000000000000000000000"
}
}

View file

@ -1,48 +1,21 @@
package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.BottomSheetScaffold
import androidx.compose.material.BottomSheetScaffoldState
import androidx.compose.material.BottomSheetState
import androidx.compose.material.BottomSheetValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FabPosition
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconLeft
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.common.form.DataField
import com.tangem.domain.common.form.FieldId
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.ContractAddress
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Decimals
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.DerivationPath
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Name
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Network
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Symbol
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.features.addCustomToken.redux.ScreenState
@ -189,12 +162,12 @@ fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
@Composable
private fun AddButton(state: MutableState<AddCustomTokenState>) {
PrimaryButtonIconLeft(
PrimaryButtonIconStart(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(id = R.string.common_add),
icon = painterResource(id = R.drawable.ic_plus_24),
iconResId = R.drawable.ic_plus_24,
enabled = state.value.screenState.addButton.isEnabled,
onClick = { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) },
)

View file

@ -24,6 +24,7 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
@ -32,6 +33,13 @@ import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -72,10 +80,7 @@ class DetailsMiddleware {
is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action)
DetailsAction.ScanCard -> {
scope.launch {
tangemSdkManager.scanProduct(
userTokensRepository = userTokensRepository,
allowsRequestAccessCodeFromRepository = true,
)
ScanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true)
.doOnSuccess { scanResponse ->
// if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))

View file

@ -98,7 +98,7 @@ private fun prepareSecurityOptions(card: CardDTO, cardTypesResolver: CardTypesRe
}
}
val allowedSecurityOptions = when {
cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() || cardTypesResolver.isSaltPay() -> {
cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> {
EnumSet.of(SecurityOption.LongTap)
}
card.settings.isBackupAllowed -> {
@ -116,7 +116,7 @@ private fun prepareSecurityOptions(card: CardDTO, cardTypesResolver: CardTypesRe
private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isSaltPay() || cardTypesResolver.isStart2Coin()
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin()
return !isNotAllowed
}

View file

@ -1,23 +1,9 @@
package com.tangem.tap.features.details.ui.common
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.RadioButton
import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -25,7 +11,7 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconRight
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.res.TangemTheme
import com.tangem.wallet.R
@ -110,13 +96,13 @@ fun EmptyTopBarWithNavigation(
@Composable
fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) {
PrimaryButtonIconRight(
PrimaryButtonIconEnd(
text = title,
enabled = enabled,
onClick = onClick,
modifier = modifier
.fillMaxWidth(),
icon = painterResource(id = R.drawable.ic_tangem_24),
iconResId = R.drawable.ic_tangem_24,
)
}

View file

@ -32,13 +32,8 @@ class DetailsViewModel(private val store: Store<AppState>) {
SettingsElement.WalletConnect -> {
if (cardTypesResolver?.isMultiwalletAllowed() == true) it else null
}
SettingsElement.SendFeedback ->
if (cardTypesResolver?.isSaltPay() != true) it else null
SettingsElement.LinkMoreCards -> {
// if (state.createBackupAllowed) it else null
// TODO: SaltPay: temporary excluding backup process for Visa cards
if (state.createBackupAllowed && cardTypesResolver?.isSaltPay() != true) it else null
}
SettingsElement.SendFeedback -> it
SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null
SettingsElement.PrivacyPolicy -> {
if (state.privacyPolicyUrl != null) it else null
}

View file

@ -40,12 +40,6 @@ class TangemDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(da
override fun getPreferenceKey(): String = "tangem_tos_accepted"
}
class SaltPayDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
override fun type(): DisclaimerType = DisclaimerType.SaltPay
override fun getUri(): Uri = Uri.parse("$baseUrl/soltpay_tos.html")
override fun getPreferenceKey(): String = "saltPay_tos_accepted"
}
class Start2CoinDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
override fun type(): DisclaimerType = DisclaimerType.Start2Coin
override fun getUri(): Uri = Uri.parse("$baseUrl/" + filename(dataProvider.getLanguage(), getRegion()))

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.disclaimer
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.models.scan.CardDTO
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
@ -12,14 +11,12 @@ import java.util.Locale
*/
enum class DisclaimerType {
Tangem,
SaltPay,
Start2Coin,
;
companion object {
fun get(cardDTO: CardDTO): DisclaimerType {
return when {
cardDTO.isSaltPay -> SaltPay
cardDTO.isStart2Coin -> Start2Coin
else -> Tangem
}
@ -31,7 +28,6 @@ fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer {
val dataProvider = provideDisclaimerDataProvider(cardDTO.cardId)
return when (this) {
DisclaimerType.Tangem -> TangemDisclaimer(dataProvider)
DisclaimerType.SaltPay -> SaltPayDisclaimer(dataProvider)
DisclaimerType.Start2Coin -> Start2CoinDisclaimer(dataProvider)
}
}

View file

@ -3,9 +3,11 @@ package com.tangem.tap.features.home.redux
import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
@ -25,11 +27,6 @@ import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.home.redux.HomeMiddleware.BUY_WALLET_URL
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.signin.redux.SignInAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
@ -80,6 +77,7 @@ private fun readCard(analyticsEvent: AnalyticsEvent?) = scope.launch {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
)
ScanCardProcessor.scan(
analyticsEvent = analyticsEvent,
onProgressStateChange = { showProgress ->
@ -93,6 +91,7 @@ private fun readCard(analyticsEvent: AnalyticsEvent?) = scope.launch {
store.dispatch(HomeAction.ScanInProgress(scanInProgress))
},
onFailure = {
Timber.e(it, "Unable to scan card")
changeButtonState(ButtonState.ENABLED)
},
onSuccess = { scanResponse ->
@ -101,26 +100,23 @@ private fun readCard(analyticsEvent: AnalyticsEvent?) = scope.launch {
)
}
fun proceedWithScanResponse(scanResponse: ScanResponse) {
scope.launch {
val userWallet = UserWalletBuilder(scanResponse).build()
if (userWallet == null) {
Timber.e("User wallet not created")
return@launch
}
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
scope.launch { store.onUserWalletSelected(userWallet = userWallet) }
}
.doOnResult {
store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card))
navigateTo(AppScreen.Wallet)
}
private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
val userWallet = UserWalletBuilder(scanResponse).build().guard {
Timber.e("User wallet not created")
return@launch
}
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
scope.launch { store.onUserWalletSelected(userWallet = userWallet) }
}
.doOnResult {
store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card))
navigateTo(AppScreen.Wallet)
}
}
private suspend fun navigateTo(appScreen: AppScreen) {

View file

@ -62,7 +62,6 @@ object OnboardingHelper {
AppScreen.OnboardingOther
}
ProductType.Twins -> AppScreen.OnboardingTwins
ProductType.SaltPay -> AppScreen.OnboardingWallet
ProductType.Start2Coin -> throw java.lang.UnsupportedOperationException(
"Onboarding for Start2Coin cards is not supported",
)

View file

@ -1,43 +0,0 @@
package com.tangem.tap.features.onboarding
import com.tangem.common.services.Result
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.extensions.successOr
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayActivationStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.update
import com.tangem.tap.preferencesStorage
/**
[REDACTED_AUTHOR]
*/
object OnboardingSaltPayHelper {
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)
}
}
@Suppress("UnusedPrivateMember")
fun testProceedToOnboarding(scanResponse: ScanResponse, manager: SaltPayActivationManager): Result<Boolean> =
Result.Success(true)
@Suppress("UnusedPrivateMember")
fun testProceedToMainScreen(scanResponse: ScanResponse, manager: SaltPayActivationManager): Result<Boolean> =
Result.Success(false)
}

View file

@ -10,8 +10,6 @@ import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
object Init : OnboardingWalletAction()
object GetToCreateWalletStep : OnboardingWalletAction()
object GetToSaltPayStep : OnboardingWalletAction()
object CreateWallet : OnboardingWalletAction()
data class WalletWasCreated(
val result: CompletionResult<CreateProductWalletTaskResponse>,
@ -57,7 +55,7 @@ sealed class BackupAction : Action {
object ScanPrimaryCard : BackupAction()
/**
* Check for unfinished backup of standard Wallets and SaltPay cards
* Check for unfinished backup of standard Wallets
* See more GlobalAction.Onboarding.StartForUnfinishedBackup
*/
object CheckForUnfinishedBackup : BackupAction()
@ -82,9 +80,6 @@ sealed class BackupAction : Action {
data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction()
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
/**
* It always calls for the SaltPay cards when resuming the activation process.
*/
data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction()
object DiscardBackup : BackupAction()

View file

@ -6,7 +6,6 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ifNotNull
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
@ -19,7 +18,6 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -30,8 +28,6 @@ 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.wallet.models.toCurrencies
import com.tangem.tap.features.wallet.redux.Artwork
import kotlinx.coroutines.launch
@ -70,7 +66,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
when (action) {
OnboardingWalletAction.Init -> {
ifNotNull(onboardingManager, card) { manager, card ->
if (!card.isSaltPay && !manager.isActivationStarted(card.cardId)) {
if (!manager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
@ -83,20 +79,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
card.wallets.isNotEmpty() && card.backupStatus?.isActive == true -> {
when {
// check for unfinished backup for saltPay cards. See more
scanResponse.cardTypesResolver.isSaltPay() && backupService.hasIncompletedBackup -> {
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
scanResponse.cardTypesResolver.isSaltPay() -> {
store.dispatch(OnboardingWalletAction.GetToSaltPayStep)
store.dispatch(BackupAction.FinishBackup(withAnalytics = false))
store.dispatch(OnboardingSaltPayAction.OnSwitchedToSaltPayProcess)
}
else -> store.dispatch(BackupAction.FinishBackup())
}
store.dispatch(BackupAction.FinishBackup())
}
else -> {
store.dispatch(OnboardingWalletAction.GetToCreateWalletStep)
@ -180,12 +163,8 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
if (onboardingWalletState.backupState.backupStep == BackupStep.InitBackup ||
onboardingWalletState.backupState.backupStep == BackupStep.Finished
) {
if (onboardingWalletState.isSaltPay) {
BackupAction.StartBackup
} else {
Analytics.send(Onboarding.Backup.ScreenOpened())
BackupAction.IntroduceBackup
}
Analytics.send(Onboarding.Backup.ScreenOpened())
BackupAction.IntroduceBackup
} else {
null
}
@ -348,11 +327,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
is BackupAction.AddBackupCard -> {
onboardingWalletState.backupCardIdFilter?.let {
backupService.skipCompatibilityChecks = true
tangemSdk.config.filter.cardIdFilter = it
}
backupService.addBackupCard { result ->
backupService.skipCompatibilityChecks = false
tangemSdk.config.filter.cardIdFilter = null
@ -400,7 +374,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
if (action.accessCodeConfirmation == backupState.accessCode) {
store.dispatch(BackupAction.SetAccessCodeError(null))
backupService.setAccessCode(action.accessCodeConfirmation)
store.dispatch(OnboardingSaltPayAction.SetAccessCode(action.accessCodeConfirmation))
store.dispatch(BackupAction.PrepareToWritePrimaryCard)
} else {
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodesDoNotMatch))
@ -421,7 +394,6 @@ 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))
@ -450,14 +422,12 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
store.dispatch(
GlobalAction.Onboarding.StartForUnfinishedBackup(
addedBackupCardsCount = backupService.addedBackupCardsCount,
isSaltPayVisa = backupService.primaryCardIsSaltPayVisa(),
),
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
is BackupAction.SkipBackup -> {
Analytics.send(Onboarding.Backup.Skipped())
if (onboardingWalletState.isSaltPay) return
Analytics.send(Onboarding.Finished())
finishCardActivation(gatherCardIds(backupState, card))
@ -466,7 +436,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
if (action.withAnalytics) {
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
}
if (onboardingWalletState.isSaltPay) return
val notActivatedCardIds = gatherCardIds(backupState, card)
.mapNotNull { if (cardActivationIsFinished(it)) null else it }
@ -492,7 +461,6 @@ private fun cardActivationIsFinished(cardId: String): Boolean {
/**
* Standard Wallet cards start activation at OnboardingWalletAction.CreateWallet
* SaltPay cards start activation at OnboardingWalletAction.StartSaltPayCardActivation
*/
private fun startCardActivation(scanResponse: ScanResponse) {
preferencesStorage.usedCardsPrefStorage.activationStarted(scanResponse.card.cardId)
@ -500,7 +468,6 @@ private fun startCardActivation(scanResponse: ScanResponse) {
/**
* Standard Wallet cards finish activation at BackupAction.SkipBackup and BackupAction.FinishBackup
* SaltPay cards finish activation at OnboardingWalletAction.FinishOnboarding
*/
internal fun finishCardActivation(cardIds: List<String>) {
cardIds.forEach { cardId ->
@ -514,31 +481,10 @@ internal fun gatherCardIds(backupState: BackupState, card: CardDTO?): List<Strin
.distinct()
}
/**
* Not used for SaltPay cards, because discarding unfinished backup is not possible
*/
private fun finishCardsActivationForDiscardedUnfinishedBackup(cardId: String) {
preferencesStorage.usedCardsPrefStorage.activationFinished(cardId)
}
/**
* SaltPay state maybe not initialized if backup resumed from ResumeFoundUnfinishedBackup action
*/
private fun initSaltPayOnBackupFinishedIfNeeded(scanResponse: ScanResponse?, state: OnboardingWalletState) {
if (state.backupState.isInterruptedBackup) return
if (scanResponse == null) return
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(withAnalytics = false))
store.dispatchOnMain(OnboardingSaltPayAction.OnSwitchedToSaltPayProcess)
}
}
private fun handleOnBackPressed(state: OnboardingWalletState) {
when (state.backupState.backupStep) {
BackupStep.InitBackup, BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode,
@ -550,12 +496,8 @@ private fun handleOnBackPressed(state: OnboardingWalletState) {
store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress))
}
BackupStep.Finished -> {
if (state.isSaltPay) {
showInterruptOnboardingDialog()
} else {
OnboardingHelper.onInterrupted()
store.dispatch(NavigationAction.PopBackTo())
}
OnboardingHelper.onInterrupted()
store.dispatch(NavigationAction.PopBackTo())
}
}
}

View file

@ -1,11 +1,8 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.domain.common.util.cardTypesResolver
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
object OnboardingWalletReducer {
@ -17,15 +14,11 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
return when (action) {
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 BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState))
is OnboardingWallet2Action -> OnboardingWallet2Reducer.reduce(action, state)
is OnboardingWalletAction.GetToCreateWalletStep -> state.copy(
step = OnboardingWalletStep.CreateWallet,
)
is OnboardingWalletAction.GetToSaltPayStep -> state.copy(
step = OnboardingWalletStep.SaltPay,
)
is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup)
is OnboardingWalletAction.SetArtworkUrl -> {
state.copy(cardArtworkUri = action.artworkUri)
@ -36,16 +29,17 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
}
private object ReducerForGlobalAction {
private const val MAX_BACKUP_CARDS = 2
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 = maxBackupCards(isSaltPay),
canSkipBackup = if (isSaltPay) false else action.canSkipBackup,
maxBackupCards = MAX_BACKUP_CARDS,
canSkipBackup = action.canSkipBackup,
),
isSaltPay = isSaltPay,
)
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {
@ -55,14 +49,11 @@ private object ReducerForGlobalAction {
canSkipBackup = false,
isInterruptedBackup = true,
),
isSaltPay = action.isSaltPayVisa,
)
}
else -> state
}
}
private fun maxBackupCards(isSaltPay: Boolean): Int = if (isSaltPay) 1 else 2
}
private object OnboardingWallet2Reducer {
@ -78,11 +69,11 @@ private object OnboardingWallet2Reducer {
private object BackupReducer {
@Suppress("ComplexMethod")
fun reduce(action: BackupAction, state: BackupState, isSaltPay: Boolean): BackupState {
fun reduce(action: BackupAction, state: BackupState): BackupState {
return when (action) {
is BackupAction.IntroduceBackup -> BackupState(
backupStep = BackupStep.InitBackup,
maxBackupCards = if (isSaltPay) 1 else 2,
maxBackupCards = 2,
canSkipBackup = state.canSkipBackup,
)
BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard)

View file

@ -2,11 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import android.graphics.Bitmap
import android.net.Uri
import com.tangem.common.CardFilter
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayActivationStep
import org.rekotlin.StateType
/**
@ -16,32 +12,21 @@ data class OnboardingWalletState(
val step: OnboardingWalletStep = OnboardingWalletStep.None,
val wallet2State: OnboardingWallet2State? = null,
val backupState: BackupState = BackupState(),
val onboardingSaltPayState: OnboardingSaltPayState? = null,
val isSaltPay: Boolean = false,
val cardArtworkUri: Uri? = null,
val showConfetti: Boolean = false,
) : StateType {
val backupCardIdFilter: CardFilter.Companion.CardIdFilter?
get() = when {
isSaltPay -> CardFilter.Companion.CardIdFilter.Allow(
items = SaltPayWorkaround.walletCardIds.toSet(),
ranges = SaltPayWorkaround.walletCardIdRanges,
)
else -> null
}
@Suppress("MagicNumber")
fun getMaxProgress(): Int {
val baseProgress = if (isSaltPay) 12 else 6
val baseProgress = 6
return getWallet2Progress() + baseProgress
}
@Suppress("ComplexMethod", "MagicNumber")
fun getProgressStep(): Int {
val progressByStep = when {
step == OnboardingWalletStep.CreateWallet -> 1
step == OnboardingWalletStep.Backup -> {
val progressByStep = when (step) {
OnboardingWalletStep.CreateWallet -> 1
OnboardingWalletStep.Backup -> {
when (backupState.backupStep) {
BackupStep.InitBackup -> 2
BackupStep.ScanOriginCard -> 3
@ -53,21 +38,7 @@ data class OnboardingWalletState(
BackupStep.Finished -> getMaxProgress()
}
}
step == OnboardingWalletStep.SaltPay && isSaltPay -> {
when (onboardingSaltPayState!!.step) {
SaltPayActivationStep.None, SaltPayActivationStep.NoGas -> 0
SaltPayActivationStep.NeedPin -> 7
SaltPayActivationStep.CardRegistration -> 8
SaltPayActivationStep.KycIntro, SaltPayActivationStep.KycStart,
SaltPayActivationStep.KycWaiting, SaltPayActivationStep.KycReject,
-> 9
SaltPayActivationStep.Claim -> 10
SaltPayActivationStep.ClaimInProgress -> 11
SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success,
-> getMaxProgress()
}
}
step == OnboardingWalletStep.Done -> getMaxProgress()
OnboardingWalletStep.Done -> getMaxProgress()
else -> 1
}
@ -82,7 +53,7 @@ data class OnboardingWallet2State(
)
enum class OnboardingWalletStep {
None, CreateWallet, SaltPay, Backup, Done
None, CreateWallet, Backup, Done
}
data class BackupState(

View file

@ -1,230 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import com.tangem.blockchain.blockchains.ethereum.CompiledEthereumTransaction
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.ethereum.SignedEthereumTransaction
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.successOr
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.successOr
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stub
import com.tangem.tap.domain.getFirstToken
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
import java.math.BigInteger
import java.math.MathContext
import java.math.RoundingMode
import java.util.concurrent.atomic.AtomicLong
/**
[REDACTED_AUTHOR]
*/
class GnosisRegistrator(
val walletManager: EthereumWalletManager,
) {
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 (blockchain) {
Blockchain.SaltPay -> "0xc659f4FEd7A84a188F54cBA4A7a49D77c1a20522"
else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain")
}
private val addressTreasureSafe = "0x24A3c2382497075b6D93258f5938f7B661c06318"
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))
}
val hasGas = wallet.amounts[AmountType.Coin]?.value?.isPositive() ?: false
return Result.Success(hasGas)
}
suspend fun prepareBeforeMakingTxs(): Result<Unit> {
walletManager.safeUpdate().successOr {
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
}
atomicNonce.set(walletManager.txCount)
return Result.Success(Unit)
}
suspend fun sendTransactions(transactions: List<SignedEthereumTransaction>): Result<List<String>> {
val results = transactions.map {
coroutineScope { async { walletManager.sendRaw(it.compiledTransaction, it.signature) } }
}.awaitAll()
val errors = results.filterIsInstance<SimpleResult.Failure>()
return if (errors.isEmpty()) {
Result.Success(listOf())
} else {
Result.Failure(errors[0].error)
}
}
suspend fun makeSetSpendLimitTx(value: BigDecimal): Result<CompiledEthereumTransaction> {
val limitAmount = Amount(walletManager.cardTokens.first(), value)
val feeAmount = walletManager.getFeeToSetSpendLimit(otpProcessorContractAddress, limitAmount)
.extractFeeAmount().successOr { return it }
val compiledEthereumTransaction = walletManager.transactionBuilder.buildSetSpendLimitToSign(
processorContractAddress = otpProcessorContractAddress,
cardAddress = walletAddress,
amount = limitAmount,
transactionFee = feeAmount,
gasLimit = walletManager.gasLimitToSetSpendLimit,
nonce = atomicNonce.getAndIncrement().toBigInteger(),
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'setSpendLimit' transaction"))
return Result.Success(compiledEthereumTransaction)
}
suspend fun makeInitOtpTx(rootOTP: ByteArray, rootOTPCounter: Int): Result<CompiledEthereumTransaction> {
val feeAmount = walletManager.getFeeToInitOTP(otpProcessorContractAddress, rootOTP, rootOTPCounter)
.extractFeeAmount().successOr { return it }
val compiledEthereumTransaction = walletManager.transactionBuilder.buildInitOTPToSign(
processorContractAddress = otpProcessorContractAddress,
cardAddress = walletAddress,
otp = rootOTP,
otpCounter = rootOTPCounter,
transactionFee = feeAmount,
gasLimit = walletManager.gasLimitToInitOTP,
nonce = atomicNonce.getAndIncrement().toBigInteger(),
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'initOtp' transaction"))
return Result.Success(compiledEthereumTransaction)
}
suspend fun makeSetWalletTx(): Result<CompiledEthereumTransaction> {
val feeAmount = walletManager.getFeeToSetWallet(otpProcessorContractAddress)
.extractFeeAmount().successOr { return it }
val compiledEthereumTransaction = walletManager.transactionBuilder.buildSetWalletToSign(
processorContractAddress = otpProcessorContractAddress,
cardAddress = walletAddress,
transactionFee = feeAmount,
gasLimit = walletManager.gasLimitToSetWallet,
nonce = atomicNonce.getAndIncrement().toBigInteger(),
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'setWallet' transaction"))
return Result.Success(compiledEthereumTransaction)
}
suspend fun makeApprovalTx(value: BigDecimal): Result<CompiledEthereumTransaction> {
val approveAmount = Amount(token, value)
val feeAmount = walletManager.getFeeToApprove(approveAmount, otpProcessorContractAddress)
.extractFeeAmount().successOr { return it }
val compiledEthereumTransaction = walletManager.transactionBuilder.buildApproveToSign(
transactionData = walletManager.createTransaction(
amount = approveAmount,
fee = feeAmount,
destination = otpProcessorContractAddress,
),
nonce = atomicNonce.getAndIncrement().toBigInteger(),
gasLimit = walletManager.gasLimitToApprove,
) ?: return Result.Failure(BlockchainSdkError.CustomError("Can't create the 'approval' transaction"))
return Result.Success(compiledEthereumTransaction)
}
suspend fun getAllowance(): Result<Amount> {
// return Result.Success(Amount(BigDecimal(0.1), walletManager.wallet.blockchain)) // test
return walletManager.getAllowance(addressTreasureSafe, token)
}
suspend fun transferFrom(amountToClaim: BigDecimal, signer: TransactionSigner): Result<Unit> {
if (walletManager.txCount == -1L) {
walletManager.safeUpdate().successOr {
return Result.Failure(BlockchainSdkError.WrappedThrowable(it.error))
}
}
// return transferFromStandardWay(amountToClaim, signer)
return transferFromHardcodeWay(amountToClaim, signer)
}
@Suppress("UnusedPrivateMember")
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 }
val transactionData = walletManager.createTransferFromTransaction(amount, feeAmount, addressTreasureSafe)
walletManager.transferFrom(transactionData, signer).successOr { return Result.Failure(it.error) }
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) {
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

@ -1,18 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import com.tangem.common.Filter
/**
[REDACTED_AUTHOR]
*/
interface 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

@ -1,341 +0,0 @@
@file:Suppress("MaximumLineLength")
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
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.paymentology.PaymentologyApiService
import com.tangem.datasource.api.paymentology.models.request.CheckRegistrationRequests
import com.tangem.datasource.api.paymentology.models.request.RegisterKYCRequest
import com.tangem.datasource.api.paymentology.models.request.RegisterWalletRequest
import com.tangem.datasource.api.paymentology.models.response.AttestationResponse
import com.tangem.datasource.api.paymentology.models.response.RegisterWalletResponse
import com.tangem.datasource.api.paymentology.models.response.RegistrationResponse
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.models.scan.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
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import kotlin.ByteArray
import kotlin.Exception
import kotlin.IllegalStateException
import kotlin.NullPointerException
import kotlin.String
import kotlin.Suppress
import kotlin.Unit
import kotlin.UnsupportedOperationException
import kotlin.byteArrayOf
import kotlin.minus
import com.tangem.blockchain.extensions.Result as BlockchainResult
/**
[REDACTED_AUTHOR]
*/
class SaltPayActivationManager(
val cardId: String,
val cardPublicKey: ByteArray,
private val kycProvider: KYCProvider,
private val paymentologyService: PaymentologyApiService,
private val gnosisRegistrator: GnosisRegistrator,
) {
val walletPublicKey = gnosisRegistrator.walletManager.wallet.publicKey.seedKey
val kycUrlProvider = KYCUrlProvider(walletPublicKey, kycProvider)
private val approvalValue: BigDecimal = BigDecimal(2).pow(256).minus(BigDecimal.ONE)
.movePointLeft(gnosisRegistrator.walletManager.wallet.blockchain.decimals())
private val spendLimitValue: BigDecimal = BigDecimal("100")
suspend fun checkHasGas(): Result<Unit> {
return when (val hasGasResult = gnosisRegistrator.checkHasGas()) {
is BlockchainResult.Success -> if (hasGasResult.data) {
Result.Success(Unit)
} else {
Result.Failure(SaltPayActivationError.NoGas)
}
is BlockchainResult.Failure -> Result.Failure(hasGasResult.error as BlockchainSdkError)
}
}
suspend fun registerKYC(): Result<Unit> {
val result = withContext(Dispatchers.IO) {
performRequest { paymentologyService.api.registerKYC(makeRegisterKYCRequest()) }
}
return when (result) {
is Result.Success -> Result.Success(Unit)
is Result.Failure -> result
}
}
suspend fun checkRegistration(): Result<RegistrationResponse.Item> {
val requestItem = CheckRegistrationRequests.Item(cardId, cardPublicKey.toHexString())
val request = CheckRegistrationRequests(listOf(requestItem))
val response: RegistrationResponse = withContext(Dispatchers.IO) {
performRequest {
paymentologyService.api.checkRegistration(request)
}
}
.successOr { return it }
.tryExtractError<RegistrationResponse>()
.successOr { return it }
return try {
if (response.results.isEmpty()) throw SaltPayActivationError.EmptyResponse
val item = response.results[0]
if (item.error != null) throw IllegalStateException(response.makeErrorMessage())
Result.Success(item)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
suspend fun requestAttestationChallenge(): Result<AttestationResponse> {
val requestItem = CheckRegistrationRequests.Item(cardId, cardPublicKey.toHexString())
return withContext(Dispatchers.IO) {
performRequest {
paymentologyService.api.requestAttestationChallenge(requestItem)
}
}
.successOr { return it }
.tryExtractError()
}
suspend fun sendTransactions(signedTransactions: List<SignedEthereumTransaction>): BlockchainResult<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,
)
return withContext(Dispatchers.IO) {
performRequest {
paymentologyService.api.registerWallet(request)
}
}
.successOr { return it }
.tryExtractError()
}
suspend fun getAmountToClaim(): Result<Amount> {
val amount = gnosisRegistrator.getAllowance().successOr {
return Result.Failure(SaltPayActivationError.FailedToGetFundsToClaim)
}
return if (amount.value == null || amount.value?.isZero() == true) {
Result.Failure(SaltPayActivationError.NoFundsToClaim)
} else {
Result.Success(amount)
}
}
suspend fun claim(amountToClaim: BigDecimal, signer: TransactionSigner): Result<Unit> {
gnosisRegistrator.transferFrom(amountToClaim, signer).successOr {
val userCancelledError = (it.error as? BlockchainSdkError.WrappedTangemError)
?.tangemError as? TangemSdkError.UserCancelled
return when (userCancelledError) {
null -> Result.Failure(SaltPayActivationError.ClaimTransactionFailed)
else -> Result.Failure(userCancelledError)
}
}
return Result.Success(Unit)
}
suspend fun getTokenAmount(): Result<BigDecimal> {
val wallet = gnosisRegistrator.walletManager.safeUpdate().successOr { return it }
val token = wallet.getFirstToken().guard {
throw UnsupportedOperationException()
}
val amountValue = wallet.amounts[AmountType.Token(token)]?.value.guard {
throw UnsupportedOperationException()
}
return Result.Success(amountValue)
}
fun makeRegistrationTask(challenge: ByteArray): SaltPayRegistrationTask {
return SaltPayRegistrationTask(
gnosisRegistrator = gnosisRegistrator,
challenge = challenge,
walletPublicKey = walletPublicKey,
approvalValue = approvalValue,
spendLimitValue = spendLimitValue,
)
}
private fun makeRegisterKYCRequest(): RegisterKYCRequest = RegisterKYCRequest(
cardId = cardId,
publicKey = cardPublicKey,
kycProvider = "UTORG",
kycRefId = kycUrlProvider.kycRefId,
)
companion object {
fun stub(): SaltPayActivationManager = SaltPayActivationManager(
kycProvider = SaltPayConfig.stub().kycProvider,
paymentologyService = PaymentologyApiService,
gnosisRegistrator = GnosisRegistrator.stub(),
cardId = "",
cardPublicKey = byteArrayOf(),
)
}
}
class KYCUrlProvider(
walletPublicKey: ByteArray,
kycProvider: KYCProvider,
) {
val kycRefId = UserWalletIdBuilder.walletPublicKey(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()
}
}
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,
)
}
@Suppress("UnusedPrivateMember")
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

@ -1,53 +0,0 @@
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
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
object SaltPayExceptionHandler {
fun handle(throwable: Throwable) {
when (throwable) {
is SaltPayActivationError -> {
val dialog = when (throwable) {
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)
}
is TangemSdkError -> {
when (throwable) {
is TangemSdkError.NetworkError -> {
val message = throwable.customMessage
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message))
}
else -> Unit
}
}
is BlockchainSdkError -> {
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(throwable.customMessage))
}
else -> {
val message = throwable.localizedMessage ?: "SaltPay unknown error"
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message))
}
}
}
}

View file

@ -1,167 +0,0 @@
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

@ -1,168 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.MediaStore
import android.text.TextUtils
import android.webkit.PermissionRequest
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.core.app.ActivityCompat
import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.isPermissionGranted
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import java.io.File
/**
[REDACTED_AUTHOR]
*/
class UtorgWebViewClient(
private val onSuccess: () -> Unit,
private val successUrl: String,
) : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean =
if (successUrl == request.url.toString()) {
onSuccess()
true
} else {
false
}
}
class UtorgWebChromeClient(
private val activity: ComponentActivity,
private val activityResultCallbackHolder: ActivityResultCallbackHolder = activity as ActivityResultCallbackHolder,
) : WebChromeClient() {
private val fileChooseRequestCode = 1002
private val permissionRequestCode = 1003
private val onActivityResultCallback: OnActivityResultCallback = ::onActivityResult
private val permissionList = listOf(
"android.permission.CAMERA",
)
private var filePathCallback: ValueCallback<Array<Uri>>? = null
private var mediaUri: Uri? = null
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>,
fileChooserParams: FileChooserParams?,
): Boolean {
activityResultCallbackHolder.addOnActivityResultCallback(onActivityResultCallback)
if (!readyToShowFileChooser()) return false
val type: String
if (fileChooserParams != null && fileChooserParams.acceptTypes != null &&
fileChooserParams.acceptTypes.isNotEmpty()
) {
type = if (!TextUtils.isEmpty(fileChooserParams.acceptTypes[0])) fileChooserParams.acceptTypes[0] else "*/*"
this.filePathCallback = filePathCallback
} else {
return false
}
proceedOnType(type, fileChooserParams.isCaptureEnabled)
return true
}
private fun readyToShowFileChooser(): Boolean {
val permissionsToGrant = permissionList.filterNot { activity.isPermissionGranted(it) }.toTypedArray()
return if (permissionsToGrant.isNotEmpty()) {
ActivityCompat.requestPermissions(activity, permissionsToGrant, permissionRequestCode)
false
} else {
true
}
}
private fun proceedOnType(type: String, isCaptureEnabled: Boolean) {
if (isCaptureEnabled) {
mediaUri = Uri.fromFile(File(getImageCaptureCachePath()))
val imageCaptureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
imageCaptureIntent.putExtra("output", mediaUri)
activity.startActivityForResult(
Intent.createChooser(imageCaptureIntent, "Image Chooser"),
fileChooseRequestCode,
)
} else {
mediaUri = Uri.fromFile(File(getImageCaptureCachePath()))
val getContentIntent = Intent(Intent.ACTION_GET_CONTENT)
getContentIntent.addCategory(Intent.CATEGORY_OPENABLE)
getContentIntent.type = "image/*"
// val imageCaptureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
// imageCaptureIntent.putExtra("output", mediaUri)
val chooserIntent = Intent.createChooser(getContentIntent, "Pick a photo")
// chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(imageCaptureIntent))
activity.startActivityForResult(
Intent.createChooser(chooserIntent, "Image Chooser"),
fileChooseRequestCode,
)
}
}
private fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
fileChooseRequestCode -> {
if (resultCode != Activity.RESULT_OK) {
filePathCallback?.onReceiveValue(null)
clean()
return
}
if (data == null) {
filePathCallback?.onReceiveValue(arrayOf(mediaUri!!))
} else {
val uri = FileChooserParams.parseResult(resultCode, data)
filePathCallback?.onReceiveValue(uri)
}
clean()
}
permissionRequestCode -> {
val notGranted = permissionList.filterNot { activity.isPermissionGranted(it) }
if (notGranted.isNotEmpty()) {
val permissionsName = notGranted.joinToString("\n") { it.permissionName() }
store.dispatchDialogShow(
AppDialog.SimpleOkDialog(
header = activity.getString(R.string.common_warning),
message = "Permission access is required to pass authorization." +
"To grant permission \n" +
"$permissionsName \n" +
"you must enable them in the app's settings",
),
)
}
}
}
}
override fun onPermissionRequest(request: PermissionRequest) {
request.grant(request.resources)
}
private fun getImageCaptureCachePath(): String = "cached_photo_" + System.currentTimeMillis() + ".jpg"
private fun clean() {
activityResultCallbackHolder.removeOnActivityResultCallback(onActivityResultCallback)
filePathCallback = null
mediaUri = null
}
private fun String.permissionName(): String {
return this.slice(this.lastIndexOf(".") until this.length)
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog
import android.app.Dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.common.extensions.guard
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
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 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) { _, _ ->
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
setCancelable(false)
}.create()
}
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog
import android.app.Dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object PutVisaCardDialog {
fun create(context: Context): Dialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.saltpay_error_empty_backup_title)
setMessage(R.string.saltpay_error_empty_backup_message)
setPositiveButton(R.string.common_ok) { _, _ -> }
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
setCancelable(false)
}.create()
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog
import android.app.Dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.moduleMessage.ConvertedDialogMessage
import com.tangem.tap.domain.moduleMessage.saltPay.SaltPayErrorConverter
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object RegistrationErrorDialog {
fun create(context: Context, dialog: SaltPayDialog.Activation.OnError): Dialog {
val convertedMessage = SaltPayErrorConverter(context).convert(dialog.error) as ConvertedDialogMessage
return AlertDialog.Builder(context).apply {
setTitle(convertedMessage.title)
setMessage(convertedMessage.message)
setPositiveButton(R.string.common_ok) { _, _ ->
store.dispatch(GlobalAction.HideDialog)
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
setCancelable(false)
}.create()
}
}

View file

@ -1,15 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
/**
[REDACTED_AUTHOR]
*/
sealed class SaltPayDialog : StateDialog {
sealed class Activation : SaltPayDialog() {
object NoGas : SaltPayDialog()
object PutVisaCard : SaltPayDialog()
data class OnError(val error: SaltPayActivationError) : SaltPayDialog()
}
}

View file

@ -1,56 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.message
import com.tangem.common.module.ModuleError
import com.tangem.common.module.ModuleErrorCode
import com.tangem.common.module.ModuleMessage
sealed interface SaltPayModuleMessage : ModuleMessage
sealed class SaltPayModuleError(
subCode: Int,
override val message: String,
override val data: Any? = null,
) : SaltPayModuleMessage, ModuleError() {
override val code: Int = ModuleErrorCode.SALT_PAY + subCode
companion object {
// base code used for all errors in the module
internal const val ERROR_CODE_REGISTRATION = 100
// const val CODE_ANY_OTHER = 200..299, 300..399 etc
}
}
sealed class SaltPayActivationError(
subCode: Int,
message: String? = null,
override val data: Any? = null,
) : SaltPayModuleError(
subCode = ERROR_CODE_REGISTRATION + subCode,
message = message ?: EMPTY_MESSAGE,
) {
object Unknown : SaltPayActivationError(subCode = 4)
object Empty : SaltPayActivationError(subCode = 4)
// gnosis
object FailedToMakeTxData : SaltPayActivationError(subCode = 1)
object FailedToSendTx : SaltPayActivationError(subCode = 2)
object NeedPin : SaltPayActivationError(subCode = 3)
object NoGas : SaltPayActivationError(subCode = 5)
object EmptyResponse : SaltPayActivationError(subCode = 6)
object CardNotFound : SaltPayActivationError(subCode = 8)
class CardDisabled(message: String?) : SaltPayActivationError(subCode = 9, message = message)
class CardNotPassed(message: String?) : SaltPayActivationError(subCode = 10, message = message)
object EmptyDynamicAttestResponse : SaltPayActivationError(subCode = 11)
object EmptyBackupCardScanned : SaltPayActivationError(subCode = 12)
object WeakPin : SaltPayActivationError(subCode = 13)
object FailedToGetFundsToClaim : SaltPayActivationError(subCode = 14)
object NoFundsToClaim : SaltPayActivationError(subCode = 15)
object ClaimTransactionFailed : SaltPayActivationError(subCode = 16)
object PutVisaCard : SaltPayActivationError(subCode = 18)
companion object {
const val EMPTY_MESSAGE = ""
}
}

View file

@ -1,46 +0,0 @@
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 org.rekotlin.Action
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
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,
) : OnboardingSaltPayAction()
data class SetInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction()
data class SetClaimRefreshInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction()
data class SetAccessCode(val accessCode: String?) : OnboardingSaltPayAction()
data class Update(val withAnalytics: Boolean = true) : OnboardingSaltPayAction()
object RegisterCard : OnboardingSaltPayAction()
object OpenUtorgKYC : OnboardingSaltPayAction()
object UtorgKYCRedirectSuccess : OnboardingSaltPayAction()
object RegisterKYC : OnboardingSaltPayAction()
object Claim : OnboardingSaltPayAction()
object RefreshClaim : OnboardingSaltPayAction()
data class TrySetPin(val pin: String) : OnboardingSaltPayAction()
data class SetPin(val pin: String) : OnboardingSaltPayAction()
data class SetStep(
val newStep: SaltPayActivationStep,
val withAnalytics: Boolean = true,
) : OnboardingSaltPayAction()
data class SetAmountToClaim(val amount: Amount?) : OnboardingSaltPayAction()
data class SetTokenBalance(val balanceValue: BigDecimal) : OnboardingSaltPayAction()
}

View file

@ -1,466 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
import com.tangem.Message
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.extensions.successOr
import com.tangem.common.Filter
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.api.paymentology.models.response.KYCStatus
import com.tangem.datasource.api.paymentology.models.response.RegistrationResponse
import com.tangem.domain.common.extensions.successOr
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.finishCardActivation
import com.tangem.tap.features.onboarding.products.wallet.redux.gatherCardIds
import com.tangem.tap.features.onboarding.products.wallet.saltPay.AllSymbolsTheSameFilter
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import com.tangem.tap.tangemSdkManager
import com.tangem.wallet.R
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
object OnboardingSaltPayMiddleware {
val handler = onboardingSaltPayMiddleware
}
private val onboardingSaltPayMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleOnboardingSaltPayAction(action, state)
next(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod")
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 getOnboardingManager(): OnboardingManager? = getAppState().globalState.onboardingState.onboardingManager
fun getState(): OnboardingSaltPayState = getOnboardingWalletState().onboardingSaltPayState!!
when (action) {
is OnboardingSaltPayAction.Init -> {
if (!getOnboardingWalletState().isSaltPay) return
val onboardingManager = getOnboardingManager().guard {
// onboardingManager may be null if it's started from standard Wallet unfinished backup
return
}
val card = onboardingManager.scanResponse.card
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
onboardingManager.activationStarted(card.cardId)
}
}
is OnboardingSaltPayAction.OnSwitchedToSaltPayProcess -> {
sendAnalyticsScreenOpened(SaltPayActivationStep.None, newStep = getState().step)
}
is OnboardingSaltPayAction.Update -> {
handleInProgress = true
val state = getState()
scope.launch {
val updateStep = state.saltPayManager.update(state.step, state.amountToClaim).successOr {
onException(it.error)
return@launch
}
handleInProgress = false
dispatchOnMain(OnboardingSaltPayAction.SetStep(updateStep, 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 -> {
handleInProgress = true
val state = getState()
scope.launch {
val saltPayManager = state.saltPayManager
checkGasIfNeeded(state.saltPayManager, state.step).successOr {
onException(it.error)
return@launch
}
val pinCode = state.pinCode.guard {
onError(SaltPayActivationError.NeedPin)
return@launch
}
val attestationResponse = saltPayManager.requestAttestationChallenge().successOr {
onException(it.error)
return@launch
}
val registrationResponse = tangemSdkManager.runTaskAsync(
runnable = saltPayManager.makeRegistrationTask(attestationResponse.challenge!!),
cardId = saltPayManager.cardId,
initialMessage = Message(
header = null,
body = tangemSdkManager.getString(R.string.registration_task_alert_message),
),
accessCode = state.accessCode,
).successOr {
onException(it.error as Exception)
return@launch
}
saltPayManager.sendTransactions(registrationResponse.signedTransactions).successOr {
onError(SaltPayActivationError.FailedToSendTx)
return@launch
}
saltPayManager.registerWallet(registrationResponse.attestResponse, pinCode).successOr {
onException(it.error)
return@launch
}
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(nextStep))
}
}
is OnboardingSaltPayAction.OpenUtorgKYC -> {
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycStart))
}
is OnboardingSaltPayAction.UtorgKYCRedirectSuccess -> {
store.dispatch(OnboardingSaltPayAction.RegisterKYC)
}
is OnboardingSaltPayAction.RegisterKYC -> {
val state = getState()
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.KycWaiting))
handleInProgress = true
scope.launch {
Timber.d("saltPayManager.registerKYC()")
state.saltPayManager.registerKYC().successOr {
onException(it.error)
return@launch
}
handleInProgress = false
dispatchOnMain(OnboardingSaltPayAction.Update())
}
}
is OnboardingSaltPayAction.TrySetPin -> {
try {
assertPinValid(action.pin, getState().pinLength)
Analytics.send(Onboarding.ButtonSetPinCode())
store.dispatch(OnboardingSaltPayAction.SetPin(action.pin))
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.CardRegistration))
} catch (error: SaltPayActivationError) {
SaltPayExceptionHandler.handle(error)
}
}
is OnboardingSaltPayAction.Claim -> {
val state = getState()
handleInProgress = true
scope.launch {
val amountToClaim = getAmountToClaimIfNeeded(state.saltPayManager, state.amountToClaim).guard {
onException(SaltPayActivationError.NoFundsToClaim)
return@launch
}
val scanResponse = store.state.globalState.scanResponse
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
val signer = TangemSigner(
card = scanResponse!!.card,
tangemSdk = tangemSdk,
initialMessage = Message(),
accessCode = state.accessCode,
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = state.saltPayManager.walletPublicKey,
remainingSignatures = signResponse.remainingSignatures,
),
)
}
state.saltPayManager.claim(amountToClaim.value!!, signer).successOr {
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.Claim))
if (it.error is TangemSdkError.UserCancelled) {
handleInProgress = false
} else {
onException(it.error)
}
return@launch
}
handleInProgress = false
Analytics.send(Onboarding.ClaimWasSuccessfully())
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimInProgress))
dispatchOnMain(OnboardingSaltPayAction.RefreshClaim)
}
}
is OnboardingSaltPayAction.RefreshClaim -> {
handleClaimRefreshInProgress = true
val state = getState()
scope.launch {
val tokenAmountValue = state.saltPayManager.getTokenAmount().successOr {
SaltPayExceptionHandler.handle(it.error)
handleClaimRefreshInProgress = false
return@launch
}
dispatchOnMain(OnboardingSaltPayAction.SetTokenBalance(tokenAmountValue))
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 -> {
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
}
val cardIds = gatherCardIds(getOnboardingWalletState().backupState, onboardingManager.scanResponse.card)
finishCardActivation(cardIds)
}
}
else -> {
/* do nothing, only reduce */
}
}
}
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?,
): Result<SaltPayActivationStep> {
Timber.d("updateSaltPayStatus")
val registrationResponse = checkRegistration().successOr { return it }
val amountToClaim = getAmountToClaimIfNeeded(this, currentAmountToClaim)
dispatchOnMain(OnboardingSaltPayAction.SetAmountToClaim(amountToClaim))
val newStep = try {
determineStep(currentStep, amountToClaim, registrationResponse)
} catch (ex: Exception) {
return Result.Failure(ex)
}
checkGasIfNeeded(this, newStep).successOr { return it }
Timber.d("update: success: %s", newStep)
return Result.Success(newStep)
}
@Suppress("ComplexMethod")
private fun determineStep(
currentStep: SaltPayActivationStep,
amountToClaim: Amount?,
response: RegistrationResponse.Item,
): SaltPayActivationStep {
fun getStepBaseOnAmountToClaim(amount: Amount?): SaltPayActivationStep = when (amount) {
null -> SaltPayActivationStep.Success
else -> SaltPayActivationStep.Claim
}
return when {
response.passed != true -> throw SaltPayActivationError.CardNotPassed(response.error)
response.disabledByAdmin == true -> throw SaltPayActivationError.CardDisabled(response.error)
// go to claim screen
response.active == true -> getStepBaseOnAmountToClaim(amountToClaim)
// pinSet is false, go toPin screen
response.pinSet == false -> SaltPayActivationStep.NeedPin
response.kycStatus != null -> {
when (currentStep) {
SaltPayActivationStep.KycWaiting -> when (response.kycStatus) {
KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycIntro
KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting
KYCStatus.CORRECTION_REQUESTED, KYCStatus.REJECTED -> SaltPayActivationStep.KycReject
KYCStatus.APPROVED -> getStepBaseOnAmountToClaim(amountToClaim)
else -> SaltPayActivationStep.KycIntro
}
SaltPayActivationStep.KycReject -> when (response.kycStatus) {
KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycStart
KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting
KYCStatus.CORRECTION_REQUESTED -> SaltPayActivationStep.KycStart
KYCStatus.REJECTED -> SaltPayActivationStep.KycStart
KYCStatus.APPROVED -> getStepBaseOnAmountToClaim(amountToClaim)
else -> SaltPayActivationStep.KycIntro
}
else -> when (response.kycStatus) {
KYCStatus.NOT_STARTED, KYCStatus.STARTED -> SaltPayActivationStep.KycIntro
KYCStatus.WAITING_FOR_APPROVAL -> SaltPayActivationStep.KycWaiting
KYCStatus.CORRECTION_REQUESTED, KYCStatus.REJECTED -> SaltPayActivationStep.KycReject
KYCStatus.APPROVED -> getStepBaseOnAmountToClaim(amountToClaim)
else -> SaltPayActivationStep.KycIntro
}
}
}
// kycDate is set, go to kyc waiting screen
response.kycDate != null -> SaltPayActivationStep.KycWaiting
else -> SaltPayActivationStep.KycIntro
}
}
private suspend fun getAmountToClaimIfNeeded(
saltPayManager: SaltPayActivationManager,
amountToClaim: Amount?,
): Amount? {
Timber.d("getAmountToClaimIfNeeded: for amount: %s", amountToClaim)
if (amountToClaim != null) {
Timber.d("getAmountToClaimIfNeeded: checks no need, amount already persist")
return amountToClaim
}
// return Amount(BigDecimal(0.1), Blockchain.SaltPay)
return when (val result = saltPayManager.getAmountToClaim()) {
is Result.Success -> {
Timber.d("getAmountToClaimIfNeeded: success: %s", result.data)
return result.data
}
is Result.Failure -> {
Timber.e(result.error, "getAmountToClaimIfNeeded: failure")
null
}
}
}
private var handleClaimRefreshInProgress: Boolean = false
set(value) {
field = value
store.dispatchOnMain(OnboardingSaltPayAction.SetClaimRefreshInProgress(value))
}
private var handleInProgress: Boolean = false
set(value) {
field = value
store.dispatchOnMain(OnboardingSaltPayAction.SetInProgress(value))
}
private suspend fun onError(error: SaltPayActivationError) {
withMainContext {
handleInProgress = false
SaltPayExceptionHandler.handle(error)
}
}
private suspend fun onException(error: Throwable) {
withMainContext {
handleInProgress = false
SaltPayExceptionHandler.handle(error)
}
}
private suspend fun checkGasIfNeeded(
saltPayManager: SaltPayActivationManager,
step: SaltPayActivationStep,
): Result<Unit> {
Timber.d("checkGasIfNeeded: for step: %s", step)
if (step.ordinal >= SaltPayActivationStep.KycIntro.ordinal) {
Timber.d("checkGasIfNeeded: no need to check for step: %s", step)
return Result.Success(Unit)
}
val result = saltPayManager.checkHasGas()
when (result) {
is Result.Success -> Timber.d("checkGasIfNeeded: has GAS")
is Result.Failure -> Timber.d("checkGasIfNeeded: has NO GAS")
}
return result
}
@Throws(SaltPayActivationError::class)
private fun assertPinValid(pin: String, pinLength: Int) {
val array = pin.toCharArray()
if (array.size < pinLength) throw SaltPayActivationError.WeakPin
val filters: List<Filter<String>> = listOf(
AllSymbolsTheSameFilter(),
)
if (filters.any { it.filter(pin) }) {
throw SaltPayActivationError.WeakPin
}
}

View file

@ -1,76 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.redux
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
object OnboardingSaltPayReducer {
fun reduce(action: Action, state: OnboardingWalletState): OnboardingWalletState {
return internalReduce(action, state)
}
}
@Suppress("ComplexMethod")
private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingWalletState): OnboardingWalletState {
val action = anyAction as? OnboardingSaltPayAction ?: return onboardingWalletState
if (onboardingWalletState.onboardingSaltPayState == null && action is OnboardingSaltPayAction.SetDependencies) {
return onboardingWalletState.copy(
onboardingSaltPayState = OnboardingSaltPayState(
saltPayManager = action.registrationManager,
),
)
}
val oldState = onboardingWalletState.onboardingSaltPayState ?: return onboardingWalletState
val newState = when (action) {
is OnboardingSaltPayAction.SetAccessCode -> {
oldState.copy(
accessCode = action.accessCode,
)
}
is OnboardingSaltPayAction.SetPin -> {
oldState.copy(pinCode = action.pin)
}
is OnboardingSaltPayAction.SetStep -> {
if (action.newStep == null) {
oldState
} else {
oldState.copy(
step = action.newStep,
)
}
}
is OnboardingSaltPayAction.SetAmountToClaim -> {
oldState.copy(
amountToClaim = action.amount,
)
}
is OnboardingSaltPayAction.SetTokenBalance -> {
oldState.copy(
tokenAmount = oldState.tokenAmount.copy(value = action.balanceValue),
)
}
is OnboardingSaltPayAction.SetInProgress -> {
oldState.copy(
inProgress = action.isInProgress,
)
}
is OnboardingSaltPayAction.SetClaimRefreshInProgress -> {
oldState.copy(
claimInProgress = action.isInProgress,
)
}
else -> oldState
}
return when (newState) {
oldState -> onboardingWalletState
else -> {
onboardingWalletState.copy(onboardingSaltPayState = newState)
}
}
}

View file

@ -1,46 +0,0 @@
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.domain.common.SaltPayWorkaround
import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
import com.tangem.tap.features.wallet.redux.ProgressState
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
data class OnboardingSaltPayState(
@Transient
val saltPayManager: SaltPayActivationManager = SaltPayActivationManager.stub(),
val pinCode: String? = null,
val accessCode: String? = null,
val amountToClaim: Amount? = null,
val tokenAmount: Amount = Amount(SaltPayWorkaround.tokenFrom(Blockchain.SaltPay), BigDecimal.ZERO),
val step: SaltPayActivationStep = SaltPayActivationStep.None,
val saltPayCardArtworkUrl: String? = null,
val inProgress: Boolean = false,
val claimInProgress: Boolean = false,
) {
val mainButtonState: WidgetState
get() = if (inProgress) ProgressState.Loading else ProgressState.Done
val pinLength: Int = 4
}
enum class SaltPayActivationStep {
None,
NoGas,
NeedPin,
CardRegistration,
KycIntro,
KycStart,
KycWaiting,
KycReject,
Claim,
ClaimInProgress,
ClaimSuccess,
Success,
}

View file

@ -1,387 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.ui
import android.net.Uri
import android.view.View
import android.view.ViewGroup
import android.view.animation.OvershootInterpolator
import android.widget.ProgressBar
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.children
import androidx.transition.TransitionManager
import coil.load
import coil.size.Scale
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.compose.PinCodeWidget
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebChromeClient
import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebViewClient
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.SaltPayActivationStep
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutOnboardingContainerBottomBinding
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
import com.tangem.wallet.databinding.LayoutOnboardingMainBinding
/**
[REDACTED_AUTHOR]
*/
internal class OnboardingSaltPayStateHandler(
private val walletFragment: OnboardingWalletFragment,
) {
private val toolbar: MaterialToolbar by lazy { walletFragment.binding.toolbar }
private val claimBinding: LayoutOnboardingMainBinding by lazy { walletFragment.bindingSaltPay.claim }
private val topContainer: LayoutOnboardingContainerTopBinding by lazy { claimBinding.onboardingTopContainer }
private val actionContainer: LayoutOnboardingContainerBottomBinding by lazy {
claimBinding.onboardingActionContainer
}
private val btnRefreshBalanceWidget by lazy {
RefreshBalanceWidget(claimBinding.onboardingTopContainer.onboardingWalletContainer)
}
private var progressButton: SaltPayProgressButton? = null
private var previousStep: SaltPayActivationStep = SaltPayActivationStep.None
fun newState(state: OnboardingWalletState) {
handleCardArtworks(state)
when (state.step) {
OnboardingWalletStep.SaltPay -> setSaltPayStep(state.onboardingSaltPayState)
OnboardingWalletStep.Backup -> {
if (state.backupState.backupStep == BackupStep.Finished && !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)
}
}
else -> walletFragment.handleOnboardingStep(state)
}
}
private fun handleCardArtworks(state: OnboardingWalletState) = with(walletFragment.binding) {
// TODO: SaltPay: remove hardCode
if (state.onboardingSaltPayState?.saltPayCardArtworkUrl == null) {
imvFrontCard.load(R.drawable.img_salt_pay_visa) {
scale(Scale.FILL)
crossfade(enable = true)
}
} else {
val uri = Uri.parse(state.onboardingSaltPayState.saltPayCardArtworkUrl)
walletFragment.loadImageIntoImageView(uri, imvFrontCard)
}
imvFirstBackupCard.load(R.drawable.card_placeholder_wallet)
// if (state.onboardingSaltPayState?.saltPayCardArtworkUrl == null) {
// //if saltPay url not loaded -> load from resource
// val bitmap = BitmapFactory.decodeResource(walletFragment.resources, R.drawable.img_salt_pay_visa)
// imvFrontCard.setImageBitmap(bitmap)
// } else {
// walletFragment.loadImageIntoImageView(state.onboardingSaltPayState.saltPayCardArtworkUrl, imvFrontCard)
// }
// walletFragment.loadImageIntoImageView(state.cardArtworkUrl, imvFirstBackupCard)
}
fun showAddBackupCards(state: BackupState) = with(walletFragment.binding) {
when (state.backupCardsNumber) {
0 -> {
tvHeader.text = walletFragment.getText(R.string.onboarding_saltpay_title_no_backup_card)
tvBody.text = walletFragment.getText(R.string.onboarding_saltpay_subtitle_no_backup_cards)
}
1 -> {
tvHeader.text = walletFragment.getText(R.string.onboarding_saltpay_title_one_backup_card)
tvBody.text = walletFragment.getText(R.string.onboarding_saltpay_subtitle_one_backup_card)
}
else -> {}
}
}
private fun setSaltPayStep(onboardingSaltPayState: OnboardingSaltPayState?) {
val state = onboardingSaltPayState.guard {
store.dispatchDebugErrorNotification("SaltPayView: setSaltPayStep: onboardingSaltPayState = null")
return
}
walletFragment.binding.onboardingWalletContainer.hide()
walletFragment.bindingSaltPay.onboardingSaltpayContainer.show()
when (state.step) {
SaltPayActivationStep.NoGas -> handleNoGas()
SaltPayActivationStep.NeedPin -> handleNeedPin()
SaltPayActivationStep.CardRegistration -> handleCardRegistration()
SaltPayActivationStep.KycIntro -> handleKycIntro()
SaltPayActivationStep.KycStart -> handleKycStart(state)
SaltPayActivationStep.KycWaiting -> handleKycWaiting()
SaltPayActivationStep.KycReject -> handleKycReject()
SaltPayActivationStep.Claim -> handleClaim(state)
SaltPayActivationStep.ClaimInProgress -> handleClaim(state)
SaltPayActivationStep.ClaimSuccess -> handleClaim(state)
SaltPayActivationStep.Success -> handleSuccess()
else -> Unit
}
progressButton?.changeState(state.mainButtonState)
}
private fun handleNoGas() {
// normally this shouldn't happen
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatchDialogShow(SaltPayDialog.Activation.NoGas)
}
private fun handleNeedPin() = with(walletFragment.bindingSaltPay) {
toolbar.title = getText(R.string.onboarding_navbar_pin)
showOnlyView(pinCode.root) {
progressButton = SaltPayProgressButton(pinCode.root)
progressButton?.mainView?.isEnabled = false
var enteredPinCode = ""
pinCode.flPinCodeContainer.setContent {
PinCodeWidget { code, isLastSymbolEntered ->
enteredPinCode = code
progressButton?.mainView?.isEnabled = isLastSymbolEntered
}
}
pinCode.btnSetPin.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.TrySetPin(enteredPinCode))
}
}
}
private fun handleCardRegistration() = with(walletFragment.bindingSaltPay) {
toolbar.title = getText(R.string.onboarding_navbar_register_wallet)
showOnlyView(connectCard.root) {
progressButton = SaltPayProgressButton(connectCard.root)
connectCard.btnConnect.setOnClickListener {
Analytics.send(Onboarding.ButtonConnect())
store.dispatch(OnboardingSaltPayAction.RegisterCard)
}
}
}
private fun handleKycIntro() = with(walletFragment.bindingSaltPay) {
toolbar.title = getText(R.string.onboarding_navbar_kyc_start)
showOnlyView(verifyIdentity.root) {
progressButton = SaltPayProgressButton(verifyIdentity.root)
verifyIdentity.btnVerify.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.OpenUtorgKYC)
}
}
}
private fun handleKycStart(state: OnboardingSaltPayState) = with(walletFragment.bindingSaltPay) {
toolbar.title = getText(R.string.onboarding_navbar_kyc_start)
showOnlyView(webvVerifyIdentity) {
progressButton = null
webvVerifyIdentity.configureSettings()
webvVerifyIdentity.webViewClient = UtorgWebViewClient(
successUrl = state.saltPayManager.kycUrlProvider.doneUrl,
onSuccess = {
webvVerifyIdentity.stop()
store.dispatch(OnboardingSaltPayAction.UtorgKYCRedirectSuccess)
},
)
webvVerifyIdentity.webChromeClient = UtorgWebChromeClient(walletFragment.requireActivity())
webvVerifyIdentity.loadUrl(state.saltPayManager.kycUrlProvider.requestUrl)
}
}
private fun handleKycWaiting() = with(walletFragment.bindingSaltPay.kycInProgress) {
toolbar.title = getText(R.string.onboarding_navbar_kyc_progress)
showOnlyView(root)
imvInProgress.setDrawable(R.drawable.ic_in_progress)
tvHeader.text = getText(R.string.onboarding_title_kyc_waiting)
tvBody.text = getText(R.string.onboarding_subtitle_kyc_waiting)
btnOpenSupportChat.hide()
btnOpenSupportChat.setOnClickListener {
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
}
btnKycAction.text = getText(R.string.onboarding_button_kyc_waiting)
btnKycAction.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.Update())
}
progressButton = SaltPayProgressButton(root)
}
private fun handleKycReject() = with(walletFragment.bindingSaltPay.kycInProgress) {
toolbar.title = getText(R.string.onboarding_navbar_kyc_progress)
showOnlyView(root)
imvInProgress.setDrawable(R.drawable.ic_reject)
tvHeader.text = getText(R.string.onboarding_title_kyc_retry)
tvBody.text = getText(R.string.onboarding_subtitle_kyc_retry)
btnOpenSupportChat.hide()
btnKycAction.text = getText(R.string.onboarding_button_kyc_start)
btnKycAction.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.Update())
}
progressButton = SaltPayProgressButton(root)
}
@Suppress("LongMethod")
private fun handleClaim(state: OnboardingSaltPayState) = with(walletFragment.bindingSaltPay) {
toolbar.title = getText(R.string.onboarding_getting_started)
val btnMain = actionContainer.btnContainer.findViewById<MaterialButton>(R.id.btn_main_action)
val tvHeader = actionContainer.tvHeader
val tvBody = actionContainer.tvBody
showOnlyView(claimBinding.root) {
// topContainer.progress.pbState.hide()
progressButton = SaltPayProgressButton(actionContainer.root)
topContainer.imvCardBackground.apply {
setBackgroundDrawable(context.getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
}
topContainer.imvFrontCard.load(R.drawable.img_salt_pay_visa) {
scale(Scale.FILL)
crossfade(enable = true)
}
actionContainer.btnAlternativeAction.hide()
updateConstraints(state.step, R.layout.lp_onboarding_topup_wallet)
}
btnRefreshBalanceWidget.changeState(if (state.claimInProgress) ProgressState.Loading else ProgressState.Done)
if (btnRefreshBalanceWidget.isShowing != true) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(OnboardingSaltPayAction.RefreshClaim)
}
}
val tokenAmountString = tokenAmountString(state)
topContainer.onboardingTvBalance.tvBalanceValue.text = tokenAmountString ?: ""
topContainer.onboardingTvBalance.tvBalanceCurrency.text = ""
when (state.step) {
SaltPayActivationStep.Claim -> {
claimValueString(state)?.let {
tvHeader.text = getText(R.string.onboarding_title_claim, it)
}
tvBody.text = getText(R.string.onboarding_subtitle_claim)
btnMain.text = getText(R.string.onboarding_button_claim)
btnMain.isEnabled
btnMain.setOnClickListener {
Analytics.send(Onboarding.ButtonClaim())
store.dispatch(OnboardingSaltPayAction.Claim)
}
progressButton = SaltPayProgressButton(actionContainer.root)
progressButton?.isEnabled = true
}
SaltPayActivationStep.ClaimInProgress -> {
tvHeader.text = getText(R.string.onboarding_title_claim_progress)
tvBody.text = getText(R.string.onboarding_subtitle_claim_progress)
btnMain.text = getText(R.string.onboarding_button_claim)
progressButton = SaltPayProgressButton(actionContainer.root)
progressButton?.isEnabled = false
}
SaltPayActivationStep.ClaimSuccess -> {
tvHeader.setText(R.string.common_success)
tvBody.setText(R.string.onboarding_subtitle_success_claim)
btnMain.setText(R.string.onboarding_button_continue_wallet)
btnMain.setOnClickListener {
walletFragment.showConfetti(false)
store.dispatch(OnboardingWalletAction.FinishOnboarding)
}
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
updateConstraints(state.step, R.layout.lp_onboarding_done_activation)
walletFragment.showConfetti(true)
progressButton = SaltPayProgressButton(actionContainer.root)
progressButton?.isEnabled = true
}
else -> {}
}
}
private fun handleSuccess() = with(walletFragment.binding) {
walletFragment.bindingSaltPay.onboardingSaltpayContainer.hide()
onboardingWalletContainer.show()
walletFragment.showSuccess()
tvBody.text = getText(R.string.onboarding_subtitle_success_claim)
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet)
}
private fun tokenAmountString(state: OnboardingSaltPayState): String? {
return state.tokenAmount.value?.toFormattedCurrencyString(
decimals = state.tokenAmount.decimals,
currency = state.tokenAmount.currencySymbol,
)
}
private fun claimValueString(state: OnboardingSaltPayState): String? {
return state.amountToClaim?.value?.toFormattedCurrencyString(
decimals = state.amountToClaim.decimals,
currency = state.amountToClaim.currencySymbol,
)
}
private fun updateConstraints(currentStep: SaltPayActivationStep, @LayoutRes layoutId: Int) {
if (this.previousStep == currentStep) return
with(claimBinding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(root.context, layoutId)
constraintSet.applyTo(onboardingWalletContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition)
}
}
private fun showOnlyView(view: View, onShowListener: (() -> Unit)? = null) {
walletFragment.bindingSaltPay.onboardingSaltpayContainer.children
.filter { it.id != view.id }
.forEach { it.hide() }
view.show { onShowListener?.invoke() }
}
private fun getText(@StringRes resId: Int): String {
return walletFragment.getString(resId)
}
private fun getText(@StringRes resId: Int, vararg formatArgs: Any?): String {
return walletFragment.getString(resId, *formatArgs)
}
private class SaltPayProgressButton(
root: ViewGroup,
) : IndeterminateProgressButtonWidget(findButton(root), findProgress(root)) {
companion object {
fun findButton(view: ViewGroup): MaterialButton {
return findContainer(view).children.firstOrNull { it is MaterialButton } as MaterialButton
}
fun findProgress(view: ViewGroup): View {
return findContainer(view).children.firstOrNull { it is ProgressBar }!!
}
fun findContainer(view: ViewGroup): ViewGroup {
return view.children.first { it.id == R.id.btn_container } as ViewGroup
}
}
}
}

View file

@ -37,14 +37,11 @@ import com.tangem.tap.features.FragmentOnBackPressedHandler
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.ui.OnboardingSaltPayStateHandler
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.mainScope
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.LayoutOnboardingSeedPhraseBinding
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
import dagger.hilt.android.AndroidEntryPoint
@ -62,13 +59,10 @@ class OnboardingWalletFragment :
internal val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer }
internal val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer }
private val seedPhraseStateHandler: OnboardingSeedPhraseStateHandler = OnboardingSeedPhraseStateHandler()
private val seedPhraseViewModel by viewModels<SeedPhraseViewModel>()
private val saltPayStateHandler: OnboardingSaltPayStateHandler = OnboardingSaltPayStateHandler(this)
private lateinit var cardsWidget: WalletCardsWidget
private var accessCodeDialog: AccessCodeDialog? = null
@ -108,7 +102,6 @@ class OnboardingWalletFragment :
store.dispatch(OnboardingWallet2Action.Init(seedPhraseViewModel.maxProgress))
store.dispatch(OnboardingWalletAction.Init)
store.dispatch(OnboardingSaltPayAction.Init)
store.dispatch(
OnboardingWalletAction.LoadArtwork(
cardArtworkUriForUnfinishedBackup = requireContext().resourceUri(R.drawable.card_placeholder_wallet),
@ -170,7 +163,6 @@ class OnboardingWalletFragment :
override fun newState(state: OnboardingWalletState) {
if (activity == null || view == null) return
if (state.isSaltPay) reInitCardsWidgetIfNeeded(1)
animator.updateBackupState(state.backupState)
requireActivity().invalidateOptionsMenu()
@ -182,9 +174,6 @@ class OnboardingWalletFragment :
state.wallet2State != null -> {
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
}
state.isSaltPay -> {
saltPayStateHandler.newState(state)
}
else -> {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvFirstBackupCard)
@ -207,7 +196,6 @@ class OnboardingWalletFragment :
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
OnboardingWalletStep.Backup -> setBackupState(
state = state.backupState,
isSaltPay = state.isSaltPay,
isWallet2 = state.wallet2State != null,
)
@ -231,21 +219,21 @@ class OnboardingWalletFragment :
animator.setupCreateWalletState()
}
private fun setBackupState(state: BackupState, isSaltPay: Boolean, isWallet2: Boolean) {
private fun setBackupState(state: BackupState, isWallet2: Boolean) {
when (state.backupStep) {
BackupStep.InitBackup -> showBackupIntro(state, isSaltPay, isWallet2)
BackupStep.InitBackup -> showBackupIntro(state, isWallet2)
BackupStep.ScanOriginCard -> showScanOriginCard()
BackupStep.AddBackupCards -> showAddBackupCards(state, isSaltPay)
BackupStep.AddBackupCards -> showAddBackupCards(state)
BackupStep.SetAccessCode -> showSetAccessCode()
BackupStep.EnterAccessCode -> showEnterAccessCode(state)
BackupStep.ReenterAccessCode -> showReenterAccessCode(state)
is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state, isSaltPay)
is BackupStep.WriteBackupCard -> showWriteBackupCard(state, isSaltPay)
is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state)
is BackupStep.WriteBackupCard -> showWriteBackupCard(state)
BackupStep.Finished -> showSuccess()
}
}
private fun showBackupIntro(state: BackupState, isSaltPay: Boolean, isWallet2: Boolean) = with(binding) {
private fun showBackupIntro(state: BackupState, isWallet2: Boolean) = with(binding) {
imvFirstBackupCard.show()
imvSecondBackupCard.show()
@ -260,7 +248,7 @@ class OnboardingWalletFragment :
btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.SkipBackup) }
btnWalletAlternativeAction.show(state.canSkipBackup && !isSaltPay && !isWallet2)
btnWalletAlternativeAction.show(state.canSkipBackup && !isWallet2)
}
animator.showBackupIntro(state)
}
@ -281,7 +269,7 @@ class OnboardingWalletFragment :
animator.showScanOriginCard()
}
private fun showAddBackupCards(state: BackupState, isSaltPay: Boolean) = with(binding) {
private fun showAddBackupCards(state: BackupState) = with(binding) {
prepareBackupView()
accessCodeDialog?.dismiss()
@ -299,25 +287,20 @@ class OnboardingWalletFragment :
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) {
saltPayStateHandler.showAddBackupCards(state)
} else {
when (state.backupCardsNumber) {
0 -> {
tvHeader.text = getText(R.string.onboarding_title_no_backup_cards)
tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards)
}
1 -> {
tvHeader.text = getText(R.string.onboarding_title_one_backup_card)
tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card)
}
2 -> {
tvHeader.text = getText(R.string.onboarding_title_two_backup_cards)
tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards)
}
else -> {}
when (state.backupCardsNumber) {
0 -> {
tvHeader.text = getText(R.string.onboarding_title_no_backup_cards)
tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards)
}
1 -> {
tvHeader.text = getText(R.string.onboarding_title_one_backup_card)
tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card)
}
2 -> {
tvHeader.text = getText(R.string.onboarding_title_two_backup_cards)
tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards)
}
else -> {}
}
animator.showAddBackupCards(state, state.backupCardsNumber)
@ -363,25 +346,19 @@ class OnboardingWalletFragment :
accessCodeDialog?.showError(state.accessCodeError)
}
private fun showWritePrimaryCard(state: BackupState, isSaltPay: Boolean) = with(binding) {
private fun showWritePrimaryCard(state: BackupState) = with(binding) {
accessCodeDialog?.dismiss()
reInitCardsWidgetIfNeeded(state.backupCardsNumber)
prepareViewForFinalizeStep()
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
if (isSaltPay) {
tvHeader.text = getText(R.string.onboarding_saltpay_title_prepare_origin)
tvBody.text = getString(R.string.onboarding_twins_interrupt_warning)
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_saltpay_button_backup_origin)
} else {
tvHeader.text = getText(R.string.common_origin_card)
tvBody.text = getString(
R.string.onboarding_subtitle_scan_primary_card_format,
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) },
)
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_backup_origin)
}
tvHeader.text = getText(R.string.common_origin_card)
tvBody.text = getString(
R.string.onboarding_subtitle_scan_primary_card_format,
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) },
)
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_backup_origin)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
animator.showWritePrimaryCard(state)
@ -399,27 +376,21 @@ class OnboardingWalletFragment :
imvSecondBackupCard.show()
}
private fun showWriteBackupCard(state: BackupState, isSaltPay: Boolean) = with(binding) {
private fun showWriteBackupCard(state: BackupState) = with(binding) {
prepareViewForFinalizeStep()
reInitCardsWidgetIfNeeded(state.backupCardsNumber)
val cardNumber = (state.backupStep as? BackupStep.WriteBackupCard)?.cardNumber ?: 1
if (isSaltPay) {
tvHeader.text = getString(R.string.onboarding_saltpay_title_backup_card)
tvBody.text = getString(R.string.onboarding_twins_interrupt_warning)
layoutButtonsCommon.btnWalletMainAction.text = getString(R.string.onboarding_saltpay_title_backup_card)
} else {
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
tvHeader.text = getString(R.string.onboarding_title_backup_card_format, cardNumber)
tvBody.text = getString(
R.string.onboarding_subtitle_scan_backup_card_format,
cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]),
)
layoutButtonsCommon.btnWalletMainAction.text = getString(
R.string.onboarding_button_backup_card_format,
cardNumber,
)
}
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
tvHeader.text = getString(R.string.onboarding_title_backup_card_format, cardNumber)
tvBody.text = getString(
R.string.onboarding_subtitle_scan_backup_card_format,
cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]),
)
layoutButtonsCommon.btnWalletMainAction.text = getString(
R.string.onboarding_button_backup_card_format,
cardNumber,
)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
store.dispatch(BackupAction.WriteBackupCard(cardNumber))
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.app.Dialog
import android.content.Context

View file

@ -26,9 +26,9 @@ interface SendScreenActionUi : SendScreenAction
object ReleaseSendState : Action
data class PrepareSendScreen(
val walletManager: WalletManager,
val coinAmount: Amount?,
val coinRate: BigDecimal?,
val walletManager: WalletManager?,
val tokenAmount: Amount? = null,
val tokenRate: BigDecimal? = null,
) : SendScreenAction

View file

@ -85,7 +85,7 @@ private class EmptyReducer : SendInternalReducer {
private class PrepareSendScreenStatesReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
val prepareAction = action as PrepareSendScreen
val walletManager = action.walletManager!!
val walletManager = action.walletManager
val amountToExtract = prepareAction.tokenAmount ?: prepareAction.coinAmount!!
val decimals = amountToExtract.decimals
val feePaidInNetworkCurrency = isFeePaidInNetworkCurrency(walletManager.wallet.blockchain)

View file

@ -98,7 +98,7 @@ sealed class WalletAction : Action {
val swapAllowed: Boolean,
) : DialogAction()
data class ChooseCurrency(val amounts: List<Amount>?) : DialogAction()
data class ChooseCurrency(val amounts: List<Amount>) : DialogAction()
data class RussianCardholdersWarningDialog(
val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data? = null,
) : DialogAction()

View file

@ -132,6 +132,5 @@ 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

@ -15,13 +15,12 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager
import kotlinx.coroutines.Dispatchers
@ -96,33 +95,27 @@ class MultiWalletMiddleware {
return
}
store.state.globalState.topUpController?.scanToGetDerivations()
scanAndUpdateCard(selectedUserWallet, walletState)
scanAndUpdateCard(selectedUserWallet)
}
else -> {}
}
}
private fun scanAndUpdateCard(selectedUserWallet: UserWallet, state: WalletState?) =
scope.launch(Dispatchers.Default) {
tangemSdkManager.scanProduct(
cardId = selectedUserWallet.cardId,
userTokensRepository = userTokensRepository,
additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain },
allowsRequestAccessCodeFromRepository = true,
)
.flatMap { scanResponse ->
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet ->
userWallet.copy(
scanResponse = scanResponse,
)
},
)
}
.doOnSuccess { updatedUserWallet ->
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(emptyList()))
store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true)
}
}
private fun scanAndUpdateCard(selectedUserWallet: UserWallet) = scope.launch(Dispatchers.Default) {
ScanCardProcessor.scan(selectedUserWallet.cardId, allowsRequestAccessCodeFromRepository = true)
.flatMap { scanResponse ->
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet ->
userWallet.copy(
scanResponse = scanResponse,
)
},
)
}
.doOnSuccess { updatedUserWallet ->
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(emptyList()))
store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true)
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.wallet.redux.middlewares
import androidx.core.os.bundleOf
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.common.extensions.guard
@ -129,13 +130,16 @@ class TradeCryptoMiddleware {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency)))
val walletManager = store.state.walletState.getWalletManager(selectedWalletData.currency).guard {
FirebaseCrashlytics.getInstance().recordException(IllegalStateException("WalletManager is null"))
return
}
val walletManager = store.state.walletState.getWalletManager(selectedWalletData.currency)
store.dispatchOnMain(
PrepareSendScreen(
coinAmount = walletManager?.wallet?.amounts?.get(AmountType.Coin),
coinRate = selectedWalletData.fiatRate,
walletManager = walletManager,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = selectedWalletData.fiatRate,
),
)
store.dispatchOnMain(

View file

@ -37,6 +37,8 @@ class WalletDialogsMiddleware {
)
}
is WalletAction.DialogAction.ChooseCurrency -> {
if (action.amounts.isEmpty()) return
store.dispatchDialogShow(
WalletDialog.SelectAmountToSendDialog(
amounts = action.amounts,

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.wallet.redux.middlewares
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.CompletionResult
import com.tangem.common.doOnSuccess
@ -32,7 +33,6 @@ import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.ifActive
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -147,26 +147,76 @@ class WalletMiddleware {
store.dispatchOpenUrl(action.exploreUrl)
}
is WalletAction.Send -> {
val walletStore = walletState.getWalletStore(walletState.selectedCurrency)
val selectedWalletData = walletState.selectedWalletData
val walletManager = walletStore?.walletManager
if (walletStore == null || walletManager == null || selectedWalletData == null) {
val error = TapError.UnsupportedState(
"WalletAction.Send: walletStore or selectedWalletData or walletManager is null",
)
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return
}
if (!networkConnectionManager.isOnline) {
store.dispatchErrorNotification(TapError.NoInternetConnection)
return
}
val newAction = prepareSendAction(action.amount, store.state.walletState)
if (newAction is PrepareSendScreen && newAction.walletManager == null) {
store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Home))
FirebaseCrashlytics.getInstance().recordException(
IllegalStateException("PrepareSendScreen: walletManager is null"),
)
store.dispatchToastNotification(R.string.internal_error_wallet_manager_not_found)
} else {
store.dispatch(newAction)
if (newAction is PrepareSendScreen) {
store.state.walletState.selectedWalletData?.currency?.let { currency ->
Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(currency)))
}
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
val currency = selectedWalletData.currency
val initSendStateAction = if (action.amount == null) {
val sendableAmounts = walletManager.wallet.getSendableAmounts()
if (sendableAmounts.isEmpty()) {
val error = TapError.UnsupportedState("WalletAction.Send: Nothing to send")
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return
}
if (walletState.isMultiwalletAllowed) {
val amountToSend = sendableAmounts.find { it.currencySymbol == currency.currencySymbol }
if (amountToSend == null) {
val error = TapError.UnsupportedState("WalletAction.Send: Amount to send is null")
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return
}
makeInitSendStateActionByCurrency(
currency = currency,
amount = amountToSend,
walletStore = walletStore,
walletManager = walletManager,
selectedWalletData = selectedWalletData,
)
} else {
val isSingleAmount = sendableAmounts.size == 1
if (isSingleAmount) {
makeInitSendStateActionByAmount(
amount = sendableAmounts.first(),
walletStore = walletStore,
walletManager = walletManager,
selectedWalletData = selectedWalletData,
)
} else {
store.dispatch(WalletAction.DialogAction.ChooseCurrency(sendableAmounts))
return
}
}
} else {
// action.amount received from the ChooseCurrency dialog
makeInitSendStateActionByAmount(
amount = action.amount,
walletManager = walletManager,
walletStore = walletStore,
selectedWalletData = selectedWalletData,
)
}
Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(currency)))
store.dispatch(initSendStateAction)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
is WalletAction.ShowSaveWalletIfNeeded -> {
showSaveWalletIfNeeded()
@ -222,6 +272,59 @@ class WalletMiddleware {
}
}
private fun makeInitSendStateActionByAmount(
amount: Amount,
walletStore: WalletStoreModel,
walletManager: WalletManager,
selectedWalletData: WalletDataModel,
): PrepareSendScreen = when (amount.type) {
AmountType.Coin ->
PrepareSendScreen(
walletManager = walletManager,
coinAmount = amount,
coinRate = selectedWalletData.fiatRate,
)
is AmountType.Token -> {
PrepareSendScreen(
walletManager = walletManager,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = walletStore.blockchainWalletData.fiatRate,
tokenAmount = amount,
tokenRate = selectedWalletData.fiatRate,
)
}
AmountType.Reserve -> {
val exception = IllegalStateException("WalletAction.Send: Reserve can't be sent")
FirebaseCrashlytics.getInstance().recordException(exception)
throw exception
}
}
private fun makeInitSendStateActionByCurrency(
currency: Currency,
amount: Amount,
walletStore: WalletStoreModel,
walletManager: WalletManager,
selectedWalletData: WalletDataModel,
): PrepareSendScreen = when (currency) {
is Currency.Blockchain -> {
PrepareSendScreen(
walletManager = walletManager,
coinAmount = amount,
coinRate = selectedWalletData.fiatRate,
)
}
is Currency.Token -> {
PrepareSendScreen(
walletManager = walletManager,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = walletStore.blockchainWalletData.fiatRate,
tokenAmount = amount,
tokenRate = selectedWalletData.fiatRate,
)
}
}
private fun changeSelectedWalletAddress(type: AddressType, state: WalletState) {
val selectedUserWalletId = userWalletsListManager.selectedUserWalletSync?.walletId.guard {
Timber.e("Unable to change selected wallet address, no user wallet selected")
@ -284,72 +387,4 @@ class WalletMiddleware {
}
}
}
private fun prepareSendAction(amount: Amount?, state: WalletState?): Action {
val selectedWalletData = state?.selectedWalletData
val currency = selectedWalletData?.currency
val walletStore = state?.getWalletStore(currency)
return if (amount != null) {
if (amount.type is AmountType.Token) {
prepareSendActionForToken(amount, selectedWalletData, walletStore)
} else {
PrepareSendScreen(amount, selectedWalletData?.fiatRate, walletStore?.walletManager)
}
} else {
val amounts = walletStore?.walletManager?.wallet?.getSendableAmounts()
if (currency != null && state.isMultiwalletAllowed) {
when (currency) {
is Currency.Blockchain -> {
val amountToSend = amounts?.find { it.currencySymbol == currency.blockchain.currency }
?: return WalletAction.DialogAction.ChooseCurrency(amounts)
PrepareSendScreen(
coinAmount = amountToSend,
coinRate = selectedWalletData.fiatRate,
walletManager = walletStore.walletManager,
)
}
is Currency.Token -> {
val amountToSend = amounts?.find { it.currencySymbol == currency.token.symbol }
?: return WalletAction.DialogAction.ChooseCurrency(amounts)
prepareSendActionForToken(
amount = amountToSend,
selectedWalletData = selectedWalletData,
walletStore = walletStore,
)
}
}
} else {
val amountsSize = amounts?.size ?: 0
if (amountsSize > 1) {
WalletAction.DialogAction.ChooseCurrency(amounts)
} else {
val amountToSend = amounts?.first()
PrepareSendScreen(
coinAmount = amountToSend,
coinRate = selectedWalletData?.fiatRate,
walletManager = walletStore?.walletManager,
)
}
}
}
}
private fun prepareSendActionForToken(
amount: Amount,
selectedWalletData: WalletDataModel?,
walletStore: WalletStoreModel?,
): PrepareSendScreen {
val coinRate = walletStore?.blockchainWalletData?.fiatRate
val tokenRate = selectedWalletData?.fiatRate
val coinAmount = walletStore?.walletManager?.wallet?.amounts?.get(AmountType.Coin)
return PrepareSendScreen(
coinAmount = coinAmount,
coinRate = coinRate,
walletManager = walletStore?.walletManager,
tokenAmount = amount,
tokenRate = tokenRate,
)
}
}

View file

@ -6,7 +6,7 @@ import com.tangem.tap.common.redux.StateDialog
import com.tangem.wallet.R
sealed interface WalletDialog : StateDialog {
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog
data class SelectAmountToSendDialog(val amounts: List<Amount>) : WalletDialog
object SignedHashesMultiWalletDialog : WalletDialog
data class ChooseTradeActionDialog(
val buyAllowed: Boolean,

View file

@ -24,7 +24,6 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.utils.OneTouchClickListener
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.MainActivity
@ -47,7 +46,6 @@ 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.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.wallet.BuildConfig
import com.tangem.wallet.R
@ -166,20 +164,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
override fun newStateOnMain(state: WalletState) {
if (activity == null || view == null) return
val isSaltPay = store.state.globalState.scanResponse?.cardTypesResolver?.isSaltPay() == true
when {
isSaltPay && walletView !is SaltPayWalletView -> {
walletView.onViewDestroy()
walletView = SaltPayWalletView()
walletView.changeWalletView(this, binding)
}
state.isMultiwalletAllowed && walletView !is MultiWalletView -> {
walletView.onViewDestroy()
walletView = MultiWalletView()
walletView.changeWalletView(this, binding)
}
!state.isMultiwalletAllowed && !isSaltPay && walletView !is SingleWalletView -> {
!state.isMultiwalletAllowed && walletView !is SingleWalletView -> {
walletView.onViewDestroy()
walletView = SingleWalletView()
walletView.changeWalletView(this, binding)
@ -199,9 +190,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
binding.toolbar.inflateMenu(R.menu.menu_wallet)
}
setupCardImage(state, isSaltPay)
setupCardImage(state)
if (!isSaltPay) showWarningsIfPresent(state.mainWarningsList)
showWarningsIfPresent(state.mainWarningsList)
setupPullToRefreshLayout(state)
@ -255,24 +246,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
binding.rvWarningMessages.show(warnings.isNotEmpty())
}
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)
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
} else {
binding.ivCard.load(state.cardImage?.artworkId) {
scale(Scale.FIT)
crossfade(enable = true)
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
private fun setupCardImage(state: WalletState) {
binding.ivCard.load(state.cardImage?.artworkId) {
scale(Scale.FIT)
crossfade(enable = true)
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
}

View file

@ -5,11 +5,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.*
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.blockchain.common.Amount
import com.tangem.tap.common.extensions.toFormattedString
@ -55,7 +51,7 @@ class AmountToSendBottomSheetDialog(
}
}
class ChooseAmountAdapter : ListAdapter<Amount, ChooseAmountAdapter.AmountViewHolder>(DiffUtilCallback) {
private class ChooseAmountAdapter : ListAdapter<Amount, ChooseAmountAdapter.AmountViewHolder>(DiffUtilCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AmountViewHolder {
val binding = ItemWalletAmountToSendBinding.inflate(

View file

@ -73,7 +73,6 @@ class MultiWalletView : WalletView() {
private fun showMultiWalletView(binding: FragmentWalletBinding) = with(binding) {
watcher.clear()
lSaltPayWallet.root.hide()
tvTwinCardNumber.hide()
rvPendingTransaction.hide()
lCardBalance.root.hide()

View file

@ -39,7 +39,6 @@ class SingleWalletView : WalletView() {
private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) {
watchedPrimaryWalletForAddressCard = null
watchedPrimaryWalletForBalance = null
lSaltPayWallet.root.hide()
tvTwinCardNumber.hide()
rvMultiwallet.hide()
btnAddToken.hide()

View file

@ -1,244 +0,0 @@
package com.tangem.tap.features.wallet.ui.wallet.saltPay
import android.view.LayoutInflater
import android.view.ViewGroup
import com.tangem.core.analytics.Analytics
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.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.utils.getFormattedAmount
import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount
import com.tangem.tap.features.wallet.ui.utils.isAvailableToBuy
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.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 timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class SaltPayWalletView : WalletView() {
private var saltPayBinding: LayoutSaltPayWalletBinding? = null
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.fiatRate == null) {
veilBalance.veil()
} else {
veilBalance.unVeil()
tvBalance.text = tokenData.getFormattedFiatAmount(appCurrency)
}
tvBalanceCrypto.text = tokenData.getFormattedAmount()
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()
}
}

View file

@ -1,79 +0,0 @@
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
}
@Suppress("MagicNumber")
private fun String.reduceAddress(): String = "${substring(0..5)}...${substring(length - 4)}"
}

View file

@ -1,113 +0,0 @@
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 = 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

@ -22,7 +22,6 @@ import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.userWalletList.unlockIfLockable
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.firstOrNull
@ -72,7 +71,7 @@ internal class WalletSelectorMiddleware {
refreshUserWalletsAmounts()
}
is WalletSelectorAction.ClearUserWallets -> {
disableUserWalletsSaving()
clearUserWalletsAndCloseError()
}
is WalletSelectorAction.AddWallet.Success,
is WalletSelectorAction.AddWallet.Error,
@ -156,16 +155,8 @@ internal class WalletSelectorMiddleware {
onFailure = { error ->
// Rollback policy if card scanning was failed
tangemSdkManager.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode)
when {
error is TangemSdkError.ExceptionError && error.cause is SaltPayActivationError -> {
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
else -> {
Timber.e(error, "Unable to scan card")
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
}
}
Timber.e(error, "Unable to scan card")
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
},
)
}
@ -217,7 +208,7 @@ internal class WalletSelectorMiddleware {
private suspend fun unlockUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult<Unit> {
Analytics.send(MyWallets.Button.WalletUnlockTapped())
tangemSdkManager.changeDisplayedCardIdNumbersCount(userWallet.scanResponse)
return tangemSdkManager.scanProduct(userTokensRepository)
return ScanCardProcessor.scan()
.map { scanResponse ->
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
if (scannedUserWalletId == userWallet.walletId) {
@ -305,12 +296,8 @@ internal class WalletSelectorMiddleware {
}
}
private fun disableUserWalletsSaving() = scope.launch {
private fun clearUserWalletsAndCloseError() = scope.launch {
clearUserWallets()
.map {
preferencesStorage.shouldSaveUserWallets = false
preferencesStorage.shouldSaveAccessCodes = false
}
.doOnSuccess {
store.dispatchWithMain(WalletSelectorAction.CloseError)
popBackToHome()

View file

@ -19,11 +19,7 @@ import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.*
import org.rekotlin.StoreSubscriber
internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSelectorState> {
@ -200,8 +196,7 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
onDismiss = this::dismissWarningDialog,
)
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
onConfirm = this::clearUserWallets,
onDismiss = this::dismissWarningDialog,
onDismiss = this::clearUserWallets,
)
else -> currentDialog
}

View file

@ -17,9 +17,10 @@ internal fun BiometricsDisabledWarningContent(warning: WarningModel.BiometricsDi
title = stringResource(id = R.string.common_warning),
message = stringResource(id = R.string.biometric_unavailable_warning),
onDismissDialog = warning.onDismiss,
isDismissable = false,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = warning.onConfirm,
onClick = warning.onDismiss,
),
)
}
@ -30,7 +31,6 @@ private fun BiometricsDisabledWarningContentSample(modifier: Modifier = Modifier
Column(modifier = modifier) {
BiometricsDisabledWarningContent(
warning = WarningModel.BiometricsDisabledWarning(
onConfirm = {},
onDismiss = {},
),
)

View file

@ -3,14 +3,7 @@ package com.tangem.tap.features.walletSelector.ui.components
import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.Icon
@ -28,7 +21,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButtonIconRight
import com.tangem.core.ui.components.SecondaryButtonIconEnd
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.res.TangemTheme
@ -191,11 +184,11 @@ private fun Footer(
onClick = onUnlockClick,
)
}
SecondaryButtonIconRight(
SecondaryButtonIconEnd(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.user_wallet_list_add_button),
showProgress = showAddCardProgress,
icon = painterResource(id = R.drawable.ic_tangem_24),
iconResId = R.drawable.ic_tangem_24,
onClick = onAddCardClick,
)
}

View file

@ -24,7 +24,6 @@ internal sealed interface WarningModel : DialogModel {
) : WarningModel
data class BiometricsDisabledWarning(
val onConfirm: () -> Unit,
val onDismiss: () -> Unit,
) : WarningModel
}

View file

@ -1,8 +1,11 @@
package com.tangem.tap.features.welcome.redux
import android.content.Intent
import com.tangem.common.*
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -16,7 +19,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.userWalletList.unlockIfLockable
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.features.signin.redux.SignInAction
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -60,16 +62,12 @@ internal class WelcomeMiddleware {
private fun disableUserWalletsSaving() = scope.launch {
userWalletsListManager.clear()
.flatMap { walletStoresManager.clear() }
.flatMap { tangemSdkManager.clearSavedUserCodes() }
.map {
preferencesStorage.shouldSaveUserWallets = false
preferencesStorage.shouldSaveAccessCodes = false
}
.doOnFailure { e ->
Timber.e(e, "Unable to clear user wallets")
}
.doOnResult {
// !!! Workaround !!!
store.dispatchWithMain(WelcomeAction.CloseError)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
}
@ -130,8 +128,8 @@ internal class WelcomeMiddleware {
scope.launch { onCardScanned(scanResponse) }
},
onFailure = {
when {
it is TangemSdkError.ExceptionError && it.cause is SaltPayActivationError -> {
when (it) {
is TangemSdkError.ExceptionError -> {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
}
else -> {

View file

@ -72,8 +72,7 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber<WelcomeState> {
onDismiss = this::dismissWarning,
)
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
onConfirm = this::clearUserWallets,
onDismiss = this::dismissWarning,
onDismiss = this::clearUserWallets,
)
else -> null
}

View file

@ -48,9 +48,10 @@ internal fun WarningDialog(warning: WarningModel?) {
title = stringResource(id = R.string.common_warning),
message = stringResource(id = R.string.biometric_unavailable_warning),
onDismissDialog = warning.onDismiss,
isDismissable = false,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = warning.onConfirm,
onClick = warning.onDismiss,
),
)
}
@ -140,7 +141,7 @@ private fun KeyInvalidatedWarningPreview_Dark() {
@Composable
private fun BiometricDisabledWarningSample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
WarningDialog(warning = WarningModel.BiometricsDisabledWarning(onConfirm = {}, onDismiss = {}))
WarningDialog(warning = WarningModel.BiometricsDisabledWarning(onDismiss = {}))
}
}

Some files were not shown because too many files have changed in this diff Show more