Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-09 17:11:34 +05:00
parent 8fd894f5d5
commit 45013d6188
105 changed files with 123 additions and 5898 deletions

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

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

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

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

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

@ -5,14 +5,11 @@ 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.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.backup.BackupService
import com.tangem.tap.*
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.common.extensions.*
@ -23,13 +20,8 @@ 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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -81,24 +73,16 @@ internal object LegacyScanProcessor {
onScanStateChange(false)
sendAnalytics(analyticsEvent, scanResponse)
checkForUnfinishedBackupForSaltPay(
backupService = backupService,
showDisclaimerIfNeed(
scanResponse = scanResponse,
disclaimerWillShow = disclaimerWillShow,
onFailure = onFailure,
nextHandler = { scanResponse1 ->
showDisclaimerIfNeed(
scanResponse = scanResponse1,
disclaimerWillShow = disclaimerWillShow,
onFailure = onFailure,
nextHandler = { scanResponse2 ->
onScanSuccess(
scanResponse = scanResponse2,
onProgressStateChange = onProgressStateChange,
onSuccess = onSuccess,
onWalletNotCreated = onWalletNotCreated,
onFailure = onFailure,
)
},
nextHandler = { scanResponse2 ->
onScanSuccess(
scanResponse = scanResponse2,
onProgressStateChange = onProgressStateChange,
onSuccess = onSuccess,
onWalletNotCreated = onWalletNotCreated,
)
},
)
@ -117,35 +101,6 @@ internal object LegacyScanProcessor {
}
}
/**
* 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))
} else {
nextHandler(scanResponse)
}
}
private suspend inline fun showDisclaimerIfNeed(
scanResponse: ScanResponse,
crossinline disclaimerWillShow: () -> Unit = {},
@ -188,7 +143,6 @@ internal object LegacyScanProcessor {
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
@ -196,66 +150,21 @@ internal object LegacyScanProcessor {
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))
}
}
}
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)
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)
}
onSuccess(scanResponse)
}
}
}

View file

@ -7,7 +7,6 @@ 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.backupService
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -52,7 +51,6 @@ internal object UseCaseScanProcessor {
if (analyticsEvent != null) {
add(AnalyticsChain(analyticsEvent))
}
add(CheckForUnfinishedSaltPayBackupChain(backupService))
add(DisclaimerChain(store, disclaimerWillShow))
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager, preferencesStorage))
}
@ -91,9 +89,7 @@ internal object UseCaseScanProcessor {
navigateTo(exception.onboardingRoute)
onWalletNotCreated()
}
is ScanChainException.PutSaltPayVisaCard,
is ScanChainException.DisclaimerWasCanceled,
is ScanChainException.CheckForSaltPayOnboardingCaseException,
-> onFailure(scanCardExceptionConverter.convertBack(exception))
}
}

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

@ -4,7 +4,6 @@ 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.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.utils.converter.TwoWayConverter
internal class ScanCardExceptionConverter : TwoWayConverter<TangemError, ScanCardException> {
@ -35,10 +34,6 @@ internal class ScanCardExceptionConverter : TwoWayConverter<TangemError, ScanCar
private fun concertScanChainException(value: ScanCardException.ChainException): TangemError {
return when (val e = value as? ScanChainException) {
is ScanChainException.DisclaimerWasCanceled -> TangemSdkError.UserCancelled()
is ScanChainException.PutSaltPayVisaCard -> TangemSdkError.ExceptionError(
SaltPayActivationError.PutVisaCard,
)
is ScanChainException.CheckForSaltPayOnboardingCaseException,
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

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

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

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

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

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

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

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

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

View file

@ -20,7 +20,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
@ -133,8 +132,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

@ -1,36 +1,26 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.models.scan.ProductType
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
import com.tangem.tap.network.exchangeServices.utorg.UtorgExchangeService
import com.tangem.tap.scope
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
* Temporary wrapper for the buy services. Service switches based on selected product type.
* Just now - UtorgService used only for the SaltPay cards
*/
internal class BuyExchangeService(
private val productTypeProvider: () -> ProductType?,
private val mercuryoService: MercuryoService,
private val utorgService: UtorgExchangeService,
) : ExchangeService, ExchangeUrlBuilder {
init {
scope.launch {
mercuryoService.update()
utorgService.update()
}
}
private val currentService: ExchangeService
get() = when (productTypeProvider.invoke()) {
ProductType.SaltPay -> utorgService
else -> mercuryoService
}
private val currentService: ExchangeService = mercuryoService
override suspend fun update() {
currentService.update()

View file

@ -1,86 +0,0 @@
package com.tangem.tap.network.exchangeServices.utorg
import android.net.Uri
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgApi
import com.tangem.tap.network.exchangeServices.utorg.mock.MockUtorgApi
import okhttp3.Interceptor
import okhttp3.Response
import java.util.*
/**
[REDACTED_AUTHOR]
*/
data class UtorgEnvironment(
val baseUri: Uri,
val sidValue: String,
val apiVersion: String,
val utorgApi: UtorgApi,
val successUrl: String = "https://success.tangem.com",
) {
companion object {
private val PROD_BASE_URL = Uri.parse("https://app.utorg.pro")
private const val PROD_VERSION = "v1"
private val STAGE_BASE_URL = Uri.parse("https://app-stage.utorg.pro")
private const val STAGE_VERSION = "v1"
fun prod(authProvider: UtorgAuthProvider, apiVersion: String = PROD_VERSION): UtorgEnvironment =
UtorgEnvironment(
baseUri = PROD_BASE_URL,
sidValue = authProvider.sidValue,
apiVersion = apiVersion,
utorgApi = createApi(PROD_BASE_URL, authProvider, false),
)
fun stage(
authProvider: UtorgAuthProvider,
logEnabled: Boolean,
apiVersion: String = STAGE_VERSION,
): UtorgEnvironment = UtorgEnvironment(
baseUri = STAGE_BASE_URL,
sidValue = authProvider.sidValue,
apiVersion = apiVersion,
utorgApi = createApi(STAGE_BASE_URL, authProvider, logEnabled),
)
fun mock(): UtorgEnvironment = UtorgEnvironment(
baseUri = Uri.EMPTY,
sidValue = "",
apiVersion = "",
utorgApi = MockUtorgApi(),
)
private fun createApi(baseUri: Uri, authProvider: UtorgAuthProvider, logEnabled: Boolean): UtorgApi {
return createRetrofitInstance(
baseUrl = "$baseUri/",
interceptors = listOf(UtorgAuthHeaderInterceptor(authProvider)),
logEnabled = logEnabled,
).create(UtorgApi::class.java)
}
}
}
data class UtorgAuthProvider(
val sidValue: String,
val headerSidKey: String = "X-AUTH-SID",
val headerNonceKey: String = "X-AUTH-NONCE",
) {
val clientNonce: String
get() = UUID.randomUUID().toString()
}
private class UtorgAuthHeaderInterceptor(
private val authProvider: UtorgAuthProvider,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
.newBuilder()
.addHeader(authProvider.headerSidKey, authProvider.sidValue)
.addHeader(authProvider.headerNonceKey, authProvider.clientNonce)
.build()
return chain.proceed(request)
}
}

View file

@ -1,116 +0,0 @@
package com.tangem.tap.network.exchangeServices.utorg
import android.net.Uri
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.utorg.api.RequestSuccessUrl
import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyData
import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyType
import com.tangem.utils.converter.Converter
/**
[REDACTED_AUTHOR]
*/
class UtorgExchangeService(
private val environment: UtorgEnvironment,
private val currencyConverter: Converter<UtorgCurrencyData, Blockchain?> = ChainToBlockchainConverter(),
) : ExchangeService {
private val api = environment.utorgApi
private val utorgCurrencies = mutableListOf<UtorgCurrencyData>()
override fun featureIsSwitchedOn(): Boolean = true
override suspend fun update() {
when (val result = performRequest { api.getCurrency(environment.apiVersion) }) {
is Result.Success -> {
if (!result.data.isSuccess()) return
val utorgCryptos = result.data.toSuccess().data.filter {
it.enabled && it.type == UtorgCurrencyType.CRYPTO
}
utorgCurrencies.clear()
utorgCurrencies.addAll(utorgCryptos)
performRequest {
api.setSuccessUrl(environment.apiVersion, RequestSuccessUrl(environment.successUrl))
}
}
is Result.Failure -> {
utorgCurrencies.clear()
}
}
}
override fun isBuyAllowed(): Boolean = true
override fun isSellAllowed(): Boolean = false
override fun availableForBuy(currency: Currency): Boolean {
if (!isBuyAllowed()) return false
val foundUtorgCurrency = utorgCurrencies.firstOrNull { utorgCurrency ->
val utorgBlockchain = currencyConverter.convert(utorgCurrency) ?: return@firstOrNull false
val isSameBlockchain = utorgBlockchain == currency.blockchain
val isSameSymbol = utorgCurrency.symbol.lowercase() == currency.currencySymbol.lowercase()
isSameBlockchain && isSameSymbol
}
return foundUtorgCurrency != null
}
override fun availableForSell(currency: Currency): Boolean = false
override fun getUrl(
action: CurrencyExchangeManager.Action,
blockchain: Blockchain,
cryptoCurrencyName: CryptoCurrencyName,
fiatCurrencyName: String,
walletAddress: String,
): String {
if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException()
// https://app-stage.utorg.pro/direct/testSID/mvmeaWyWiuVYdZdySgofAt6CmKhJbRhaWA/?&currency=BTC
val builder = Uri.Builder()
.scheme(environment.baseUri.scheme)
.authority(environment.baseUri.authority)
.appendPath("direct")
.appendPath(environment.sidValue)
.appendPath(walletAddress)
.appendQueryParameter("currency", cryptoCurrencyName)
// if we set the paymentCurrency, then the Utorg widget didn't work
// .appendQueryParameter("paymentCurrency", "USD")
val url = builder.build().toString()
return url
}
override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null
}
private class ChainToBlockchainConverter : Converter<UtorgCurrencyData, Blockchain?> {
override fun convert(value: UtorgCurrencyData): Blockchain? {
return when (value.chain?.uppercase()) {
"ARBITRUM" -> Blockchain.Arbitrum
"AVALANCHE" -> Blockchain.Avalanche
"BINANCE_SMART_CHAIN" -> Blockchain.BSC
"BITCOIN" -> Blockchain.Bitcoin
"BNB" -> Blockchain.Binance
"ETHEREUM" -> Blockchain.Ethereum
"GNOSIS" -> Blockchain.Gnosis
"POLYGON" -> Blockchain.Polygon
"RIPPLE" -> Blockchain.XRP
"RSK" -> Blockchain.RSK
"SOLANA" -> Blockchain.Solana
else -> null
// "APTOS", "ATOM", "NEAR", "ZKSYNC", "VECHAIN", "VELAS", "VELAS_EVM" -> null
}
}
}

View file

@ -1,62 +0,0 @@
package com.tangem.tap.network.exchangeServices.utorg.api
import com.squareup.moshi.Json
import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyResponse
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.Path
/**
[REDACTED_AUTHOR]
*/
interface UtorgApi {
@POST("api/merchant/{apiVersion}/settings/currency")
suspend fun getCurrency(@Path("apiVersion") apiVersion: String): UtorgCurrencyResponse
@POST("api/merchant/{apiVersion}/settings/successUrl")
suspend fun setSuccessUrl(@Path("apiVersion") apiVersion: String, @Body request: RequestSuccessUrl)
}
data class RequestSuccessUrl(
val url: String,
)
interface UtorgResponse<Data> {
val success: Boolean
val timestamp: Long
val data: Data?
val error: UtorgErrorResponse?
fun isSuccess(): Boolean = success && data != null && error == null
@Throws(NullPointerException::class)
fun toSuccess(): UtorgSuccessResponse<Data> {
val internalTimestamp = this.timestamp
val internalData = this.data
return object : UtorgSuccessResponse<Data> {
override val timestamp: Long = internalTimestamp
override val data: Data = internalData!!
}
}
@Throws(NullPointerException::class)
fun toError(): UtorgErrorResponse = error!!
}
interface UtorgSuccessResponse<T> {
val timestamp: Long
val data: T
}
data class UtorgErrorResponse(
@Json(name = "message") val message: String?,
@Json(name = "type") val type: UtorgErrorType,
)
enum class UtorgErrorType {
UNAUTHORIZED,
UNKNOWN_ERROR,
BAD_REQUEST,
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.network.exchangeServices.utorg.api.model
import com.squareup.moshi.Json
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgErrorResponse
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgResponse
class UtorgCurrencyResponse(
@Json(name = "success") override val success: Boolean,
@Json(name = "timestamp") override val timestamp: Long,
@Json(name = "data") override val data: List<UtorgCurrencyData>?,
@Json(name = "error") override val error: UtorgErrorResponse?,
) : UtorgResponse<List<UtorgCurrencyData>>
data class UtorgCurrencyData(
@Json(name = "currency") val currency: String,
@Json(name = "symbol") val symbol: String,
@Json(name = "enabled") val enabled: Boolean,
@Json(name = "type") val type: UtorgCurrencyType,
@Json(name = "caption") val caption: String?,
@Json(name = "chain") val chain: String?,
)
enum class UtorgCurrencyType {
FIAT, CRYPTO
}

View file

@ -1,58 +0,0 @@
package com.tangem.tap.network.exchangeServices.utorg.mock
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.network.exchangeServices.utorg.api.RequestSuccessUrl
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgApi
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgErrorResponse
import com.tangem.tap.network.exchangeServices.utorg.api.UtorgErrorType
import com.tangem.tap.network.exchangeServices.utorg.api.model.UtorgCurrencyResponse
import kotlinx.coroutines.delay
/**
[REDACTED_AUTHOR]
*/
class MockUtorgApi : UtorgApi {
var nextDelay = 500L
var nextResponseType = ResponseType.Success
var nextErrorType = UtorgErrorType.UNKNOWN_ERROR
override suspend fun getCurrency(apiVersion: String): UtorgCurrencyResponse {
delay(nextDelay)
return UtorgCurrencyResponse(
success = createIsSuccess(),
timestamp = createTime(),
data = createData(MockUtorgSuccessDataResponse.GetCurrency),
error = createError(),
)
}
override suspend fun setSuccessUrl(apiVersion: String, request: RequestSuccessUrl) {
delay(nextDelay)
}
private fun createIsSuccess(): Boolean = when (nextResponseType) {
ResponseType.Success -> true
ResponseType.Error -> false
}
private fun createTime(): Long = System.currentTimeMillis()
private inline fun <reified T> createData(response: MockUtorgSuccessDataResponse): T? = when (nextResponseType) {
ResponseType.Success -> MoshiJsonConverter.INSTANCE.fromJson(response.json)!!
ResponseType.Error -> null
}
private fun createError(): UtorgErrorResponse? = when (nextResponseType) {
ResponseType.Success -> null
ResponseType.Error -> UtorgErrorResponse(
message = "Optional message",
type = nextErrorType,
)
}
enum class ResponseType {
Success, Error
}
}

View file

@ -1,12 +0,0 @@
package com.tangem.tap.network.exchangeServices.utorg.mock
import com.tangem.tap.network.exchangeServices.utorg.mock.json.getCurrencyFull
/**
[REDACTED_AUTHOR]
*/
sealed class MockUtorgSuccessDataResponse(
val json: String,
) {
object GetCurrency : MockUtorgSuccessDataResponse(getCurrencyFull)
}

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/button_disabled" android:state_enabled="false" />
<item android:color="@color/button_secondary" />
</selector>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#66FFFFFF" android:state_enabled="false" />
<item android:color="@color/text_primary_1" />
</selector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View file

@ -1,87 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_in_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:src="@drawable/ic_in_progress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="@dimen/onboarding_wallet_top_guide_line_weight" />
<TextView
android:id="@+id/tv_header"
style="@style/TextViewOnboarding.Header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/onboarding_title_kyc_waiting"
app:layout_constraintBottom_toTopOf="@+id/tv_body"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/guideline1" />
<TextView
android:id="@+id/tv_body"
style="@style/TextViewOnboarding.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="55dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="55dp"
android:text="@string/onboarding_subtitle_kyc_waiting"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_open_support_chat"
style="@style/TapSecondaryButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="92dp"
android:text="@string/chat_button_title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/btn_container"
app:layout_constraintStart_toStartOf="@id/btn_container" />
<FrameLayout
android:id="@+id/btn_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_kyc_action"
style="@style/TapPrimaryButton"
android:layout_height="wrap_content"
android:text="@string/onboarding_button_kyc_waiting" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:elevation="18dp"
android:indeterminate="true"
android:indeterminateTint="@color/backgroundLightGray"
android:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -221,13 +221,6 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/onboarding_saltpay_container"
layout="@layout/layout_onboarding_saltpay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<include
android:id="@+id/onboarding_seed_phrase_container"
layout="@layout/layout_onboarding_seed_phrase"
@ -242,4 +235,4 @@
layout="@layout/view_confetti"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -177,15 +177,6 @@
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
<include
android:id="@+id/l_salt_pay_wallet"
layout="@layout/layout_salt_pay_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
<include
android:id="@+id/l_address"
layout="@layout/layout_address"

View file

@ -1,89 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="56dp"
android:animateLayoutChanges="true">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_tx"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/ic_tx_incoming" />
<TextView
android:id="@+id/tv_tx_hash"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="68dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:letterSpacing="0.01"
android:singleLine="true"
android:textColor="@color/text_primary_1"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/tv_tx_amount_sign"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="33BdfSkjandfjkajksjjkgkajsbga2B" />
<TextView
android:id="@+id/tv_tx_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:letterSpacing="0.03"
android:textColor="@color/text_tertiary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/tv_tx_hash"
tools:text="12:43" />
<TextView
android:id="@+id/tv_tx_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginBottom="8dp"
android:ellipsize="end"
android:letterSpacing="0.03"
android:singleLine="true"
android:text="@string/transaction_history_tx_in_progress"
android:textColor="@color/text_attention"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tv_tx_amount_sign"
app:layout_constraintStart_toStartOf="@+id/tv_tx_hash" />
<TextView
android:id="@+id/tv_tx_amount_sign"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:letterSpacing="0.02"
android:textColor="@color/text_primary_1"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="@+id/tv_tx_amount_value"
app:layout_constraintEnd_toStartOf="@+id/tv_tx_amount_value"
app:layout_constraintTop_toTopOf="@+id/tv_tx_amount_value"
tools:text="+" />
<TextView
android:id="@+id/tv_tx_amount_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:letterSpacing="0.02"
android:textColor="@color/text_primary_1"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="443" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_title_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:letterSpacing="0.02"
android:textColor="@color/text_tertiary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Today" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,60 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.skydoves.androidveil.VeilLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:veilLayout_baseColor="@color/lightGray0"
app:veilLayout_highlightColor="@color/lightGray1"
app:veilLayout_radius="4dp"
app:veilLayout_shimmerEnable="true"
app:veilLayout_veiled="true"
tools:veilLayout_veiled="false">
<!--android:background="@drawable/rectangle_twin_background"-->
<!--android:backgroundTint="@color/text_attention"-->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="56dp">
<View
android:id="@+id/imv_tx"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/tv_tx_hash"
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_marginStart="68dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
app:layout_constraintEnd_toStartOf="@+id/tv_tx_amount"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/tv_tx_time"
android:layout_width="50dp"
android:layout_height="16dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/tv_tx_hash" />
<View
android:id="@+id/tv_tx_amount"
android:layout_width="80dp"
android:layout_height="20dp"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.skydoves.androidveil.VeilLayout>

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/onboarding_saltpay_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/pin_code"
layout="@layout/layout_onboarding_saltpay_pin_code"
android:visibility="gone" />
<include
android:id="@+id/connect_card"
layout="@layout/layout_onboarding_saltpay_connect_card"
android:visibility="gone" />
<include
android:id="@+id/verify_identity"
layout="@layout/layout_onboarding_saltpay_kyc_start"
android:visibility="gone" />
<WebView
android:id="@+id/webv_verify_identity"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
android:visibility="gone" />
<include
android:id="@+id/kyc_in_progress"
layout="@layout/layout_onboarding_saltpay_kyc_in_progress"
android:visibility="gone" />
<include
android:id="@+id/claim"
layout="@layout/layout_onboarding_main"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_connecting_socket"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_connecting_socket"
app:layout_constraintBottom_toTopOf="@+id/tv_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/tv_header"
style="@style/TextViewOnboarding.Header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:text="@string/onboarding_title_register_wallet"
app:layout_constraintBottom_toTopOf="@+id/tv_body"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imv_connecting_socket" />
<TextView
android:id="@+id/tv_body"
style="@style/TextViewOnboarding.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="55dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="55dp"
android:text="@string/onboarding_subtitle_register_wallet"
app:layout_constraintBottom_toTopOf="@+id/btn_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header" />
<FrameLayout
android:id="@+id/btn_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_connect"
style="@style/TapPrimaryButton"
android:layout_height="wrap_content"
android:text="@string/onboarding_button_register_wallet"
app:icon="@drawable/ic_tangem_24"
app:iconGravity="textEnd" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:elevation="18dp"
android:indeterminate="true"
android:indeterminateTint="@color/backgroundLightGray"
android:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,87 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_in_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:src="@drawable/ic_in_progress"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.4" />
<TextView
android:id="@+id/tv_header"
style="@style/TextViewOnboarding.Header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/onboarding_title_kyc_waiting"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline1" />
<TextView
android:id="@+id/tv_body"
style="@style/TextViewOnboarding.Body"
android:layout_width="302dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header"
tools:text="@string/onboarding_subtitle_kyc_waiting" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_open_support_chat"
style="@style/TapSecondaryButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="92dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/btn_container"
app:layout_constraintStart_toStartOf="@id/btn_container"
tools:text="@string/chat_button_title" />
<FrameLayout
android:id="@+id/btn_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_kyc_action"
style="@style/TapPrimaryButton"
android:layout_height="wrap_content"
android:text="@string/onboarding_button_kyc_waiting" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:elevation="18dp"
android:indeterminate="true"
android:indeterminateTint="@color/backgroundLightGray"
android:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_passport"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_passport"
app:layout_constraintBottom_toTopOf="@+id/tv_header"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/tv_header"
style="@style/TextViewOnboarding.Header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:text="@string/onboarding_title_kyc_start"
app:layout_constraintBottom_toTopOf="@+id/tv_body"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imv_passport" />
<TextView
android:id="@+id/tv_body"
style="@style/TextViewOnboarding.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="55dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="55dp"
android:text="@string/onboarding_subtitle_kyc_start"
app:layout_constraintBottom_toTopOf="@+id/btn_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header" />
<FrameLayout
android:id="@+id/btn_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_verify"
style="@style/TapPrimaryButton"
android:layout_height="wrap_content"
android:text="@string/onboarding_button_kyc_start" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:elevation="18dp"
android:indeterminate="true"
android:indeterminateTint="@color/backgroundLightGray"
android:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,72 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_header"
style="@style/TextViewOnboarding.Header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="94dp"
android:text="@string/onboarding_title_pin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_body"
style="@style/TextViewOnboarding.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="55dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="55dp"
android:text="@string/onboarding_subtitle_pin"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_header" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/fl_pin_code_container"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_body" />
<FrameLayout
android:id="@+id/btn_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_set_pin"
style="@style/TapPrimaryButton"
android:layout_height="wrap_content"
android:text="@string/onboarding_button_pin" />
<ProgressBar
android:id="@+id/progress"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="center"
android:elevation="18dp"
android:indeterminate="true"
android:indeterminateTint="@color/backgroundLightGray"
android:visibility="gone" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,134 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingTop="12dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/onboarding_balance_title"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.skydoves.androidveil.VeilLayout
android:id="@+id/veil_balance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
app:layout_constraintBottom_toTopOf="@id/tv_unreachable"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_title"
app:veilLayout_baseColor="@color/lightGray0"
app:veilLayout_highlightColor="@color/lightGray1"
app:veilLayout_layout="@layout/card_total_balance_shimmer"
app:veilLayout_radius="4dp"
app:veilLayout_shimmerEnable="true"
app:veilLayout_veiled="true"
tools:veilLayout_veiled="false"
tools:visibility="visible">
<TextView
android:id="@+id/tv_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:textColor="@color/text_primary_1"
android:textSize="24sp"
android:textStyle="bold"
tools:text="22 325.40 $" />
</com.skydoves.androidveil.VeilLayout>
<com.skydoves.androidveil.VeilLayout
android:id="@+id/veil_balance_crypto"
android:layout_width="wrap_content"
android:layout_height="18dp"
android:layout_marginTop="6dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/veil_balance"
app:veilLayout_baseColor="@color/lightGray0"
app:veilLayout_highlightColor="@color/lightGray1"
app:veilLayout_layout="@layout/card_total_balance_shimmer"
app:veilLayout_radius="4dp"
app:veilLayout_shimmerEnable="true"
app:veilLayout_veiled="true"
tools:veilLayout_veiled="false"
tools:visibility="visible">
<TextView
android:id="@+id/tv_balance_crypto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:maxLines="1"
android:minWidth="152dp"
android:textColor="@color/text_tertiary"
android:textSize="12sp"
tools:text="5.13123123123 ETH"
tools:visibility="visible" />
</com.skydoves.androidveil.VeilLayout>
<TextView
android:id="@+id/tv_unreachable"
android:layout_width="wrap_content"
android:layout_height="18dp"
android:layout_marginTop="6dp"
android:text="@string/wallet_balance_blockchain_unreachable"
android:textColor="@color/warning"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/veil_balance" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_buy"
style="@style/BaseSaltPayButton"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_marginTop="76dp"
android:text="@string/wallet_button_buy"
android:visibility="gone"
app:icon="@drawable/ic_add"
app:iconGravity="textStart"
app:iconTint="@color/selector_saltpay_btn_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_title"
tools:visibility="visible" />
<TextView
android:id="@+id/tv_currency_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="@color/text_tertiary"
android:textSize="14sp"
android:textStyle="bold"
app:drawableEndCompat="@drawable/ic_arrow_angle_down"
app:drawableTint="@color/text_tertiary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="USD" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,136 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:clipToPadding="false"
android:minHeight="272dp"
android:paddingBottom="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_tx_history_shimmer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="42dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_salt_pay_tx_history_shimmer" />
<TextView
android:id="@+id/tv_title_tx"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:letterSpacing="0.01"
android:text="@string/transaction_history_title"
android:textColor="@color/text_tertiary"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_title_explore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:drawableLeft="@drawable/ic_tx_explore"
android:drawablePadding="4dp"
android:letterSpacing="0.01"
android:text="Explore"
android:textColor="@color/text_tertiary"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_tx_list_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@+id/tv_tx_list_empty"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/ic_accept_coin" />
<TextView
android:id="@+id/tv_tx_list_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="24dp"
android:text="@string/transaction_history_empty_transactions"
android:textColor="@color/text_tertiary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imv_tx_list_empty" />
<TextView
android:id="@+id/tv_tx_list_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:text="@string/transaction_history_error_failed_to_load"
android:textColor="@color/text_tertiary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_tx_history"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="42dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_salt_pay_tx_history" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_shimmer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="rv_tx_history_shimmer" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="imv_tx_list_empty, tv_tx_list_empty" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="tv_tx_list_error" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="rv_tx_history" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cl_salt_pay_wallet_info"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingTop="6dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp">
<include
android:id="@+id/l_salt_pay_balance"
layout="@layout/layout_salt_pay_balance"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/l_salt_pay_tx_history"
layout="@layout/layout_salt_pay_tx_history"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/l_salt_pay_balance" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -97,19 +97,6 @@
<item name="android:lineSpacingExtra">4sp</item>
</style>
<style name="BaseSaltPayButton">
<item name="android:minHeight">40dp</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:elevation">0dp</item>
<item name="android:textColor">@color/selector_saltpay_btn_text</item>
<item name="android:textAllCaps">false</item>
<item name="android:letterSpacing">0</item>
<item name="android:backgroundTint">@color/selector_saltpay_btn</item>
<item name="cornerRadius">@dimen/btn_corner_radius_medium</item>
<item name="android:stateListAnimator">@null</item>
</style>
<style name="heading">
<item name="android:textSize">24sp</item>
<item name="android:textColor">#060606</item>

View file

@ -9,6 +9,5 @@ object ModuleErrorCode {
const val COMMON = 10000
const val NETWORK = 20000
const val DOMAIN = 30000
const val SALT_PAY = 40000
const val ONBOARDING = 50000
}

View file

@ -2,7 +2,6 @@ package com.tangem.datasource.config
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.BlockchairCredentials
import com.tangem.blockchain.common.BlockscoutCredentials
import com.tangem.blockchain.common.GetBlockCredentials
import com.tangem.blockchain.common.NowNodeCredentials
import com.tangem.blockchain.common.QuickNodeCredentials
@ -88,10 +87,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
authToken = configValues.blockchairAuthorizationToken,
),
blockcypherTokens = configValues.blockcypherTokens,
blockscoutCredentials = BlockscoutCredentials(
userName = configValues.saltPay.blockscoutCredentials.user,
password = configValues.saltPay.blockscoutCredentials.password,
),
quickNodeSolanaCredentials = QuickNodeCredentials(
apiKey = configValues.quiknodeApiKey,
subdomain = configValues.quiknodeSubdomain,
@ -102,7 +97,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
),
infuraProjectId = configValues.infuraProjectId,
tronGridApiKey = configValues.tronGridApiKey,
saltPayAuthToken = configValues.saltPay.credentials.basicAuthToken,
nowNodeCredentials = NowNodeCredentials(configValues.nowNodesApiKey),
getBlockCredentials = GetBlockCredentials(configValues.getBlockApiKey),
kaspaSecondaryApiUrl = configValues.kaspaSecondaryApiUrl,
@ -115,7 +109,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
amplitudeApiKey = configValues.amplitudeApiKey,
shopify = configValues.shopifyShop,
zendesk = configValues.zendesk,
saltPayConfig = configValues.saltPay,
swapReferrerAccount = configValues.swapReferrerAccount,
)
}

View file

@ -17,6 +17,5 @@ data class Config(
val isCreatingTwinCardsAllowed: Boolean = false,
val shopify: ShopifyShop? = null,
val zendesk: ZendeskConfig? = null,
val saltPayConfig: SaltPayConfig? = null,
val swapReferrerAccount: SwapReferrerAccount? = null,
)

View file

@ -7,7 +7,6 @@ import com.squareup.moshi.Json
*/
class FeatureModel(
val isWalletPayIdEnabled: Boolean,
val isTopUpEnabled: Boolean,
val isSendingToPayIdEnabled: Boolean,
val isCreatingTwinCardsAllowed: Boolean,
@ -34,7 +33,6 @@ class ConfigValueModel(
val appsFlyer: AppsFlyer,
val shopifyShop: ShopifyShop?,
val zendesk: ZendeskConfig?,
val saltPay: SaltPayConfig,
val tronGridApiKey: String,
val amplitudeApiKey: String,
val swapReferrerAccount: SwapReferrerAccount?,

View file

@ -1,36 +0,0 @@
package com.tangem.datasource.config.models
/**
[REDACTED_AUTHOR]
*/
data class SaltPayConfig(
val sprinklr: SprinklrConfig,
val kycProvider: KYCProvider,
val credentials: Credentials,
val blockscoutCredentials: Credentials,
) {
companion object {
fun stub(): SaltPayConfig {
return SaltPayConfig(
sprinklr = SprinklrConfig("", ""),
kycProvider = KYCProvider("", "", "", ""),
credentials = Credentials("", ""),
blockscoutCredentials = Credentials("", ""),
)
}
}
}
data class KYCProvider(
val baseUrl: String,
val externalIdParameterKey: String,
val sidParameterKey: String,
val sidValue: String,
)
data class Credentials(
val user: String,
val password: String,
) {
val basicAuthToken: String by lazy { okhttp3.Credentials.basic(user, password) }
}

View file

@ -73,7 +73,6 @@ class ScanCardUseCase(
private fun updateCardIdDisplayFormat(productType: ProductType) {
tangemSdk.config.cardIdDisplayFormat = when (productType) {
ProductType.Twins -> CardIdDisplayFormat.LastLuhn(numbers = 4)
ProductType.SaltPay -> CardIdDisplayFormat.None
ProductType.Note,
ProductType.Wallet,
ProductType.Start2Coin,

View file

@ -7,9 +7,6 @@ interface CardTypesResolver {
fun isTangemNote(): Boolean
fun isTangemWallet(): Boolean
fun isWallet2(): Boolean
fun isSaltPay(): Boolean
fun isSaltPayVisa(): Boolean
fun isSaltPayWallet(): Boolean
fun isTangemTwins(): Boolean
fun isStart2Coin(): Boolean

View file

@ -1,84 +0,0 @@
package com.tangem.domain.common
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.CardIdRange
import com.tangem.common.contains
object SaltPayWorkaround {
@Suppress("MagicNumber")
val visaBatches = listOf(
"AE02",
"AE03",
) + attachTestVisaBatches()
val walletCardIds = listOf(
"AC01000000033503",
"AC01000000033594",
"AC01000000033586",
"AC01000000034477",
"AC01000000032760",
"AC01000000033867",
"AC01000000032653",
"AC01000000032752",
"AC01000000034485",
"AC01000000033644",
"AC01000000037454",
"AC01000000037462",
"AC03000000076070",
"AC03000000076088",
"AC03000000076096",
"AC03000000076104",
"AC03000000076112",
"AC03000000076120",
"AC03000000076138",
"AC03000000076146",
"AC03000000076153",
"AC03000000076161",
"AC03000000076179",
"AC03000000076187",
"AC03000000076195",
"AC03000000076203",
"AC03000000076211",
"AC03000000076229",
) + attachTestWalletCardIds()
val walletCardIdRanges = listOf(
CardIdRange("AC05000000000003", "AC05000000015993")!!,
) + attachTestWalletCardIdRanges()
fun tokenFrom(blockchain: Blockchain): Token {
return when (blockchain) {
Blockchain.SaltPay -> Token(
name = "WXDAI",
symbol = "wxDAI",
contractAddress = "0x4200000000000000000000000000000000000006",
decimals = 18,
id = "wrapped-xdai",
)
else -> error("It is not SaltPay")
}
}
@Suppress("MagicNumber")
fun isSaltPayCardId(cardId: String): Boolean = isVisaBatchId(cardId.take(4)) || isWalletCardId(cardId)
fun isVisaBatchId(batchId: String): Boolean = visaBatches.contains(batchId)
fun isWalletCardId(cardId: String): Boolean {
return if (walletCardIds.contains(cardId)) true else walletCardIdRanges.contains(cardId)
}
private fun attachTestVisaBatches(): List<String> = listOf(
"FF03",
)
private fun attachTestWalletCardIds(): List<String> = listOf(
"FF04000000000232",
)
private fun attachTestWalletCardIdRanges(): List<CardIdRange> = listOf(
CardIdRange("FF04000000000000", "FF04999999999999")!!,
)
}

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