Updated on 2026-08-14

This commit is contained in:
Tangem 2022-12-12 16:07:15 +03:00
commit 8553e2b722
156 changed files with 2318 additions and 742 deletions

View file

@ -108,10 +108,10 @@ repositories {
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
implementation(project(":domain"))
implementation(project(":network"))
implementation(project(":common"))
implementation(project(":core:res"))
implementation(project(":core:ui"))
implementation(project(":core:datasource"))
implementation(project(":core:utils"))
implementation(project(":libs:crypto"))
implementation(project(":libs:auth"))
@ -120,6 +120,9 @@ dependencies {
implementation(project(":features:referral:presentation"))
implementation(project(":features:referral:domain"))
implementation(project(":features:referral:data"))
implementation(project(":features:swap:presentation"))
implementation(project(":features:swap:domain"))
implementation(project(":features:swap:data"))
/** AndroidX libraries */
implementation(AndroidX.coreKtx)

@ -1 +1 @@
Subproject commit bfc2bf8157089bce6b44779bdae66df2c920de70
Subproject commit 897f0f7c402fd53c5ee1fa4ab601ecd4e43ff389

View file

@ -6,7 +6,11 @@ import androidx.lifecycle.lifecycleScope
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.time.Duration

View file

@ -15,7 +15,6 @@ import com.tangem.operations.backup.BackupService
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.extensions.dispatchOnMain
@ -29,6 +28,7 @@ import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
@ -66,7 +66,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val intentHandler = IntentHandler()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
@ -119,16 +118,23 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted
val shopOpened = store.state.shopState.total != null
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore && !shopOpened)) {
val navigateTo = if (userWalletsListManager.hasSavedUserWallets) AppScreen.Welcome else AppScreen.Home
store.dispatchOnMain(NavigationAction.NavigateTo(navigateTo))
if (userWalletsListManager.hasSavedUserWallets) {
store.dispatchOnMain(WelcomeAction.HandleDeepLink(intent))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Welcome))
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Home))
intentHandler.handleWalletConnectLink(intent)
}
}
intentHandler.handleIntent(intent)
intentHandler.handleBackgroundScan(intent)
intentHandler.handleSellCurrencyCallback(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intentHandler.handleIntent(intent)
intentHandler.handleBackgroundScan(intent)
intentHandler.handleSellCurrencyCallback(intent)
intentHandler.handleWalletConnectLink(intent)
}
override fun onStart() {

View file

@ -11,11 +11,12 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.AndroidAssetReader
import com.tangem.tap.common.AssetReader
import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
@ -109,6 +110,7 @@ val walletCurrenciesManager by lazy {
val totalFiatBalanceCalculator by lazy {
TotalFiatBalanceCalculator.provideDefaultImplementation()
}
val intentHandler by lazy { IntentHandler() }
@HiltAndroidApp
class TapApplication : Application(), ImageLoaderFactory {
@ -156,6 +158,7 @@ class TapApplication : Application(), ImageLoaderFactory {
appStateHolder.userTokensRepository = userTokensRepository
}
//todo refactor: move to datasource and provide via DI
private fun initMoshiConverter() {
fun appAdapters(): List<Any> = listOf(
BigDecimalAdapter(),

View file

@ -19,13 +19,13 @@ class IntentHandler {
private val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
private val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
fun handleIntent(intent: Intent?) {
handleBackgroundScan(intent)
handleWalletConnectLink(intent)
handleSellCurrencyCallback(intent)
fun handleWalletConnectLink(intent: Intent?) {
if (intent?.scheme == WalletConnectManager.WC_SCHEME) {
store.dispatch(WalletConnectAction.HandleDeepLink(intent.data?.toString()))
}
}
private fun handleBackgroundScan(intent: Intent?) {
fun handleBackgroundScan(intent: Intent?) {
if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action ||
NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action
)
@ -40,13 +40,7 @@ class IntentHandler {
}
}
private fun handleWalletConnectLink(intent: Intent?) {
if (intent?.scheme == WalletConnectManager.WC_SCHEME) {
store.dispatch(WalletConnectAction.HandleDeepLink(intent.data?.toString()))
}
}
private fun handleSellCurrencyCallback(intent: Intent?) {
fun handleSellCurrencyCallback(intent: Intent?) {
try {
val transactionID =
intent?.data?.getQueryParameter(TRANSACTION_ID_PARAM) ?: return

View file

@ -54,12 +54,12 @@ fun FragmentActivity.openFragment(
if (screen.isDialogFragment) {
(fragment as DialogFragment).show(transaction, screen.name)
if (addToBackstack) {
transaction.addToBackStack(null)
transaction.addToBackStack(screen.name)
}
} else {
transaction.replace(R.id.fragment_container, fragment, screen.name)
if (addToBackstack && (screen != AppScreen.Home && screen != AppScreen.Welcome)) {
transaction.addToBackStack(null)
if (addToBackstack) {
transaction.addToBackStack(screen.name)
}
transaction.commitAllowingStateLoss()
}

View file

@ -12,7 +12,10 @@ sealed class NavigationAction : Action {
val addToBackstack: Boolean = true,
) : NavigationAction()
data class PopBackTo(val screen: AppScreen? = null) : NavigationAction()
data class PopBackTo(
val screen: AppScreen? = null,
val inclusive: Boolean = false,
) : NavigationAction()
data class OpenUrl(val url: String) : NavigationAction()

View file

@ -32,13 +32,13 @@ val navigationMiddleware: Middleware<AppState> = { _, state ->
AppScreen.Home,
AppScreen.Welcome,
-> {
navState?.activity?.get()?.popBackTo(screen = null, inclusive = true)
navState?.activity?.get()?.popBackTo(screen, action.inclusive)
if (navState?.backStack?.contains(screen) != true) {
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
}
}
else -> {
navState?.activity?.get()?.popBackTo(screen = action.screen)
navState?.activity?.get()?.popBackTo(screen, action.inclusive)
}
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.domain
import com.tangem.common.services.Result
import com.tangem.domain.common.ThrottlerWithValues
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import java.math.BigDecimal

View file

@ -48,9 +48,9 @@ import kotlin.coroutines.suspendCoroutine
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
val canUseBiometry: Boolean
get() = tangemSdk.biometricManager.canAuthenticate || canEnrollBiometrics
get() = tangemSdk.biometricManager.canAuthenticate || needEnrollBiometrics
val canEnrollBiometrics: Boolean
val needEnrollBiometrics: Boolean
get() = tangemSdk.biometricManager.canEnrollBiometrics
val biometricManager: BiometricManager
@ -61,10 +61,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
cardId: String? = null,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
messageRes: Int? = null,
useBiometricsForAccessCode: Boolean = false,
): CompletionResult<ScanResponse> {
setAccessCodeRequestPolicy(useBiometricsForAccessCode)
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
runnable = ScanProductTask(
@ -109,9 +106,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
suspend fun derivePublicKeys(
cardId: String,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
useBiometricsForAccessCode: Boolean = false,
): CompletionResult<DerivationTaskResponse> {
setAccessCodeRequestPolicy(useBiometricsForAccessCode)
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
}
@ -192,9 +187,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
suspend fun scanCard(
cardId: String? = null,
useBiometricsForAccessCode: Boolean = false,
): CompletionResult<CardDTO> {
setAccessCodeRequestPolicy(useBiometricsForAccessCode)
return runTaskAsyncReturnOnMain(
runnable = ScanTask(),
cardId = cardId,

View file

@ -2,7 +2,7 @@ package com.tangem.tap.domain
import androidx.annotation.StringRes
import com.tangem.common.core.TangemError
import com.tangem.network.api.tangemTech.TangemTechError
import com.tangem.datasource.api.tangemTech.TangemTechError
import com.tangem.wallet.R
interface TapErrors

View file

@ -147,7 +147,7 @@ class TapWalletManager {
updateConfigManager(data)
withMainContext {
store.dispatch(WalletAction.ResetState(data.card.cardId))
store.dispatch(WalletAction.ResetState(data.card))
store.dispatch(WalletConnectAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))

View file

@ -1,8 +1,10 @@
package com.tangem.tap.domain.model.builders
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.model.UserWallet
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
@ -17,29 +19,27 @@ interface WalletStoreBuilder {
fun walletManager(walletManager: WalletManager?): WalletStoreBuilder
}
interface WalletMangerWalletStoreBuilder : WalletStoreBuilder {
fun blockchainNetwork(blockchainNetwork: BlockchainNetwork?): WalletStoreBuilder
}
interface WalletMangerWalletStoreBuilder : WalletStoreBuilder
companion object {
operator fun invoke(
userWallet: UserWallet,
userWalletId: UserWalletId,
blockchainNetwork: BlockchainNetwork,
): BlockchainNetworkWalletStoreBuilder {
return BlockchainNetworkWalletStoreBuilderImpl(userWallet, blockchainNetwork)
return BlockchainNetworkWalletStoreBuilderImpl(userWalletId, blockchainNetwork)
}
operator fun invoke(
userWallet: UserWallet,
userWalletId: UserWalletId,
walletManager: WalletManager,
): WalletMangerWalletStoreBuilder {
return WalletMangerWalletStoreBuilderImpl(userWallet, walletManager)
return WalletMangerWalletStoreBuilderImpl(userWalletId, walletManager)
}
}
}
private class BlockchainNetworkWalletStoreBuilderImpl(
private val userWallet: UserWallet,
private val userWalletId: UserWalletId,
private val blockchainNetwork: BlockchainNetwork,
) : WalletStoreBuilder.BlockchainNetworkWalletStoreBuilder {
private var walletManager: WalletManager? = null
@ -53,7 +53,7 @@ private class BlockchainNetworkWalletStoreBuilderImpl(
val tokensWalletsData = blockchainNetwork.getTokensWalletsData(walletManager)
return WalletStoreModel(
userWalletId = userWallet.walletId,
userWalletId = userWalletId,
blockchainNetwork = blockchainNetwork,
walletManager = walletManager,
walletsData = (listOf(blockchainWalletData) + tokensWalletsData),
@ -63,25 +63,20 @@ private class BlockchainNetworkWalletStoreBuilderImpl(
}
private class WalletMangerWalletStoreBuilderImpl(
private val userWallet: UserWallet,
private val userWalletId: UserWalletId,
private val walletManager: WalletManager,
) : WalletStoreBuilder.WalletMangerWalletStoreBuilder {
private var blockchainNetwork: BlockchainNetwork? = null
override fun blockchainNetwork(blockchainNetwork: BlockchainNetwork?) = this.apply {
this.blockchainNetwork = blockchainNetwork
}
override fun build(): WalletStoreModel {
val blockchainNetwork = this.blockchainNetwork ?: BlockchainNetwork.fromWalletManager(walletManager)
val blockchainWalletData = blockchainNetwork.getBlockchainWalletData(walletManager)
val tokensWalletsData = blockchainNetwork.getTokensWalletsData(walletManager)
val wallet = walletManager.wallet
val blockchainWalletData = wallet.blockchain.toBlockchainWalletData(walletManager)
val tokenWalletsData = wallet.getTokens().firstOrNull()?.toTokenWalletData(walletManager)
return WalletStoreModel(
userWalletId = userWallet.walletId,
blockchainNetwork = blockchainNetwork,
userWalletId = userWalletId,
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager),
walletManager = walletManager,
walletsData = (listOf(blockchainWalletData) + tokensWalletsData),
walletsData = (listOf(blockchainWalletData) + listOfNotNull(tokenWalletsData)),
walletRent = null,
)
}
@ -117,6 +112,35 @@ private fun BlockchainNetwork.getTokensWalletsData(walletManager: WalletManager?
}
}
private fun Blockchain.toBlockchainWalletData(walletManager: WalletManager): WalletDataModel {
val wallet = walletManager.wallet
return WalletDataModel(
currency = Currency.Blockchain(
blockchain = this,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
),
status = WalletDataModel.Loading,
walletAddresses = wallet.createAddressesData(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
)
}
private fun Token.toTokenWalletData(walletManager: WalletManager): WalletDataModel {
val wallet = walletManager.wallet
return WalletDataModel(
currency = Currency.Token(
token = this,
blockchain = wallet.blockchain,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
),
status = WalletDataModel.Loading,
walletAddresses = wallet.createAddressesData(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
)
}
private fun getExistentialDeposit(walletManager: WalletManager?): BigDecimal? {
return (walletManager as? ExistentialDepositProvider)?.getExistentialDeposit()
}

View file

@ -44,7 +44,6 @@ import kotlinx.coroutines.launch
// TODO: Create repository for that
object ScanCardProcessor {
suspend fun scan(
useBiometricsForAccessCode: Boolean = false,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
cardId: String? = null,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
@ -55,29 +54,37 @@ object ScanCardProcessor {
) = withMainContext {
onProgressStateChange(true)
onScanStateChange(true)
tangemSdkManager.scanProduct(
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
val result = tangemSdkManager.scanProduct(
userTokensRepository = userTokensRepository,
cardId = cardId,
additionalBlockchainsToDerive = additionalBlockchainsToDerive,
useBiometricsForAccessCode = useBiometricsForAccessCode,
)
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
result
.doOnFailure { error ->
onProgressStateChange(false)
onScanStateChange(false)
onFailure(error)
}
.doOnSuccess { scanResponse ->
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
onScanStateChange(false)
checkForUnfinishedBackupForSaltPay(
backupService = backupService,
scanResponse = scanResponse,
onProgressStateChange = { onProgressStateChange(it) },
nextHandler = {
nextHandler = { scanResponse1 ->
showDisclaimerIfNeed(
scanResponse = scanResponse,
nextHandler = {
scanResponse = scanResponse1,
nextHandler = { scanResponse2 ->
onScanSuccess(
scanResponse = scanResponse,
scanResponse = scanResponse2,
onProgressStateChange = onProgressStateChange,
onSuccess = onSuccess,
onWalletNotCreated = onWalletNotCreated,
@ -87,7 +94,6 @@ object ScanCardProcessor {
)
},
)
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.network.api.tangemTech.CoinsResponse
import com.tangem.datasource.api.tangemTech.CoinsResponse
@JsonClass(generateAdapter = true)
data class CurrencyFromJson(

View file

@ -6,9 +6,9 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getListOfCoins
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.CoinsResponse
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.CoinsResponse
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.tap.common.AssetReader
class LoadAvailableCoinsService(

View file

@ -7,8 +7,8 @@ import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getTokens
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.tap.common.FileReader
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.TokenDao

View file

@ -2,8 +2,8 @@ package com.tangem.tap.domain.tokens
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.UserTokensResponse
import com.tangem.tap.domain.NoDataError
class UserTokensNetworkService(private val tangemTechService: TangemTechService) {

View file

@ -3,10 +3,10 @@ package com.tangem.tap.domain.tokens
import android.content.Context
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.services.Result
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.UserTokensResponse
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.util.userWalletId
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.tap.common.AndroidFileReader
import com.tangem.tap.domain.NoDataError
import com.tangem.tap.domain.tokens.models.BlockchainNetwork

View file

@ -2,9 +2,9 @@ package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.tangem.Log
import com.tangem.datasource.api.tangemTech.UserTokensResponse
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.domain.common.CardDTO
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.FileReader
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toCurrencies

View file

@ -5,10 +5,30 @@ import com.tangem.tap.domain.model.WalletStoreModel
import java.math.BigDecimal
interface TotalFiatBalanceCalculator {
/**
* Calculate total fiat balance for list of [WalletStoreModel]
* @param prevAmount Previous amount, used in [TotalFiatBalance.Refreshing]
* @param walletStores List of [WalletStoreModel] to calculate fiat amount
* @param initial Initial [TotalFiatBalance] state, used when list of [WalletStoreModel] is empty
* @return [TotalFiatBalance] with state found with the [WalletStoreModel] list
* */
suspend fun calculate(
prevAmount: BigDecimal,
prevAmount: BigDecimal?,
walletStores: List<WalletStoreModel>,
initial: TotalFiatBalance,
): TotalFiatBalance
/**
* Same as [TotalFiatBalanceCalculator.calculate] but returns null if list of [WalletStoreModel] is empty
* @param prevAmount Previous amount, used in [TotalFiatBalance.Refreshing]
* @param walletStores List of [WalletStoreModel] to calculate fiat amount
* @return [TotalFiatBalance] with state found with the [WalletStoreModel] list
* */
suspend fun calculateOrNull(
prevAmount: BigDecimal?,
walletStores: List<WalletStoreModel>,
): TotalFiatBalance?
companion object
}

View file

@ -11,11 +11,19 @@ import java.math.BigDecimal
internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
override suspend fun calculate(
prevAmount: BigDecimal,
prevAmount: BigDecimal?,
walletStores: List<WalletStoreModel>,
initial: TotalFiatBalance,
): TotalFiatBalance {
return calculateOrNull(prevAmount, walletStores) ?: initial
}
override suspend fun calculateOrNull(
prevAmount: BigDecimal?,
walletStores: List<WalletStoreModel>,
): TotalFiatBalance? {
return if (walletStores.isEmpty()) {
TotalFiatBalance.Loading
null
} else {
withContext(Dispatchers.Default) {
val walletsData = walletStores
@ -25,7 +33,9 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
when (walletsData.findStatus()) {
TotalFiatBalanceStatus.Loading -> TotalFiatBalance.Loading
TotalFiatBalanceStatus.Refreshing -> TotalFiatBalance.Refreshing(prevAmount)
TotalFiatBalanceStatus.Refreshing -> TotalFiatBalance.Refreshing(
amount = prevAmount ?: BigDecimal.ZERO,
)
TotalFiatBalanceStatus.Error -> TotalFiatBalance.Error(calculateAmount())
TotalFiatBalanceStatus.Loaded -> TotalFiatBalance.Loaded(calculateAmount())
}

View file

@ -10,7 +10,7 @@ import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.network.common.MoshiConverter
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.common.AssetReader
import com.tangem.tap.tangemSdkManager

View file

@ -19,8 +19,14 @@ interface UserWalletsListManager {
suspend fun selectWallet(walletId: UserWalletId): CompletionResult<UserWallet>
suspend fun save(userWallet: UserWallet): CompletionResult<Unit>
suspend fun update(userWallet: UserWallet): CompletionResult<Unit>
/**
* Save user's wallet
* @param userWallet User's wallet to save
* @param canOverride If false, then terminate with [UserWalletListError.WalletAlreadySaved] when user tries to save an
* already saved card
* @return [CompletionResult] operation result
* */
suspend fun save(userWallet: UserWallet, canOverride: Boolean = false): CompletionResult<Unit>
suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit>
suspend fun clear(): CompletionResult<Unit>

View file

@ -110,12 +110,36 @@ internal class BiometricUserWalletsListManager(
findSelectedWallet()!!
}
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
return saveInternal(userWallet, override = false)
}
override suspend fun save(
userWallet: UserWallet,
canOverride: Boolean,
): CompletionResult<Unit> = withUnlock {
val isWalletSaved = state.value.wallets
.filter { it.isSaved }
.flatMap(UserWallet::cardsInWallet)
.contains(userWallet.cardId)
override suspend fun update(userWallet: UserWallet): CompletionResult<Unit> {
return saveInternal(userWallet, override = true)
if (isWalletSaved && !canOverride) {
CompletionResult.Failure(UserWalletListError.WalletAlreadySaved)
} else {
keysRepository.save(
walletId = userWallet.walletId,
encryptionKey = userWallet.scanResponse.card.encryptionKey,
)
.doOnSuccess { keys ->
state.update { prevState ->
prevState.copy(
encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId },
)
}
}
.flatMap { publicInformationRepository.save(userWallet) }
.flatMap { sensitiveInformationRepository.save(userWallet) }
.flatMap { loadModels() }
.doOnSuccess {
userWallet.isSaved = true
}
}
}
override suspend fun delete(walletIds: List<UserWalletId>): CompletionResult<Unit> {
@ -162,38 +186,6 @@ internal class BiometricUserWalletsListManager(
}
}
private suspend fun saveInternal(
userWallet: UserWallet,
override: Boolean,
): CompletionResult<Unit> = withUnlock {
val isWalletSaved = state.value.wallets
.filter { it.isSaved }
.flatMap(UserWallet::cardsInWallet)
.contains(userWallet.cardId)
if (isWalletSaved && !override) {
CompletionResult.Failure(UserWalletListError.WalletAlreadySaved)
} else {
keysRepository.save(
walletId = userWallet.walletId,
encryptionKey = userWallet.scanResponse.card.encryptionKey,
)
.doOnSuccess { keys ->
state.update { prevState ->
prevState.copy(
encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId },
)
}
}
.flatMap { publicInformationRepository.save(userWallet) }
.flatMap { sensitiveInformationRepository.save(userWallet) }
.flatMap { loadModels() }
.doOnSuccess {
userWallet.isSaved = true
}
}
}
private suspend inline fun <reified T> withUnlock(
block: () -> CompletionResult<T>,
): CompletionResult<T> {

View file

@ -40,11 +40,7 @@ class DummyUserWalletsListManager : UserWalletsListManager {
}
}
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun update(userWallet: UserWallet): CompletionResult<Unit> {
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}

View file

@ -187,8 +187,8 @@ internal class DefaultWalletCurrenciesManager(
.flatMap { walletManager ->
walletStoresRepository.storeOrUpdate(
userWalletId = userWalletId,
walletStore = WalletStoreBuilder(userWallet, walletManager)
.blockchainNetwork(blockchainNetwork)
walletStore = WalletStoreBuilder(userWalletId, blockchainNetwork)
.walletManager(walletManager)
.build(),
)
}

View file

@ -122,7 +122,7 @@ internal class DefaultWalletStoresManager(
{ walletManager ->
walletStoresRepository.storeOrUpdate(
userWalletId = userWalletId,
walletStore = WalletStoreBuilder(userWallet, blockchainNetwork)
walletStore = WalletStoreBuilder(userWalletId, blockchainNetwork)
.walletManager(walletManager)
.build(),
)
@ -158,9 +158,10 @@ internal class DefaultWalletStoresManager(
refresh = refresh,
)
.flatMap { walletManager ->
val userWalletId = userWallet.walletId
walletStoresRepository.storeOrUpdate(
userWalletId = userWallet.walletId,
walletStore = WalletStoreBuilder(userWallet, walletManager)
userWalletId = userWalletId,
walletStore = WalletStoreBuilder(userWalletId, walletManager)
.build(),
)
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.domain.walletStores.repository.di
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository

View file

@ -14,9 +14,9 @@ import com.tangem.common.flatMap
import com.tangem.common.flatMapOnFailure
import com.tangem.common.map
import com.tangem.common.services.Result
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.UserWalletId
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.replaceByOrAdd
import com.tangem.tap.domain.model.UserWallet

View file

@ -7,7 +7,7 @@ import com.squareup.moshi.JsonClass
import com.squareup.moshi.Types
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.network.common.MoshiConverter
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.trustwallet.walletconnect.models.WCPeerMeta
@ -18,7 +18,7 @@ import java.nio.charset.Charset
class WalletConnectRepository(val context: Application) {
private val moshi = MoshiConverter.defaultMoshi()
private val walletConnectAdapter: JsonAdapter<List<SessionDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, SessionDao::class.java)
Types.newParameterizedType(List::class.java, SessionDao::class.java),
)
fun saveSession(session: WalletConnectSession) {
@ -76,7 +76,6 @@ class WalletConnectRepository(val context: Application) {
}
}
@JsonClass(generateAdapter = true)
data class SessionDao(
val peerId: String,
@ -91,7 +90,7 @@ data class SessionDao(
remotePeerId = remotePeerId,
wallet = wallet,
session = session,
peerMeta = peerMeta
peerMeta = peerMeta,
)
}
@ -102,7 +101,7 @@ data class SessionDao(
remotePeerId = session.remotePeerId,
wallet = session.wallet,
session = session.session,
peerMeta = session.peerMeta
peerMeta = session.peerMeta,
)
}
}

View file

@ -55,10 +55,14 @@ sealed class DetailsAction : Action {
) : AppSettings()
}
object EnrollBiometrics : AppSettings() {
object Enroll : AppSettings()
object Cancel : AppSettings()
}
data class CheckBiometricsStatus(
val awaitStatusChange: Boolean,
) : AppSettings()
object EnrollBiometrics : AppSettings()
data class BiometricsStatusChanged(
val needEnrollBiometrics: Boolean,
) : AppSettings()
}
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()

View file

@ -30,6 +30,7 @@ import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
@ -77,11 +78,7 @@ class DetailsMiddleware {
}
DetailsAction.ScanCard -> {
scope.launch {
tangemSdkManager.scanCard(
cardId = state.scanResponse?.card?.cardId,
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
state.scanResponse?.card?.isAccessCodeSet == true,
)
tangemSdkManager.scanCard(cardId = state.scanResponse?.card?.cardId)
.doOnSuccess { card ->
val currentCardId = store.state.globalState.scanResponse?.card
?.userWalletId
@ -121,12 +118,15 @@ class DetailsMiddleware {
.flatMap { userWalletsListManager.delete(listOf(card.userWalletId)) }
.doOnSuccess {
Analytics.send(Settings.CardSettings.FactoryResetFinished())
val screen = if (userWalletsListManager.hasSavedUserWallets) {
AppScreen.Welcome
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedUserWallet)
} else {
AppScreen.Home
userWalletsListManager.lock()
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
store.dispatchOnMain(NavigationAction.PopBackTo(screen))
}
.doOnFailure { error ->
(error as? TangemSdkError)?.let { sdkError ->
@ -195,18 +195,39 @@ class DetailsMiddleware {
fun handle(state: DetailsState, action: DetailsAction.AppSettings) {
when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> {
if (tangemSdkManager.canEnrollBiometrics) {
store.dispatch(DetailsAction.AppSettings.EnrollBiometrics)
}
when (action.setting) {
PrivacySetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable)
PrivacySetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable)
}
}
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> Unit
is DetailsAction.AppSettings.EnrollBiometrics -> Unit
is DetailsAction.AppSettings.EnrollBiometrics.Enroll -> enrollBiometrics()
is DetailsAction.AppSettings.EnrollBiometrics.Cancel -> Unit
is DetailsAction.AppSettings.CheckBiometricsStatus -> {
checkBiometricsStatus(action.awaitStatusChange, state)
}
is DetailsAction.AppSettings.EnrollBiometrics -> {
enrollBiometrics()
}
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.BiometricsStatusChanged,
-> Unit
}
}
/**
* @param awaitStatusChange If true then start a new coroutine and check the biometric status every 100
* milliseconds until it changes
* */
private fun checkBiometricsStatus(awaitStatusChange: Boolean, state: DetailsState) {
scope.launch {
if (awaitStatusChange) {
while (state.needEnrollBiometrics == tangemSdkManager.needEnrollBiometrics) {
delay(timeMillis = 100)
}
}
store.dispatchOnMain(
DetailsAction.AppSettings.BiometricsStatusChanged(
needEnrollBiometrics = tangemSdkManager.needEnrollBiometrics,
),
)
}
}
@ -217,7 +238,7 @@ class DetailsMiddleware {
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
if (state.saveWallets == enable) return@launch
if (enable) {
saveCurrentWallet()
saveCurrentWallet(state)
} else {
deleteSavedWallets()
if (state.saveAccessCodes) {
@ -230,16 +251,16 @@ class DetailsMiddleware {
if (state.saveAccessCodes == enable) return@launch
if (enable) {
if (!state.saveWallets) {
saveCurrentWallet()
saveCurrentWallet(state)
}
saveAccessCodes()
saveAccessCodes(state)
} else {
deleteSavedAccessCodes()
}
}
private suspend fun saveCurrentWallet() {
val scanResponse = store.state.detailsState.scanResponse ?: return
private suspend fun saveCurrentWallet(state: DetailsState) {
val scanResponse = state.scanResponse ?: return
val userWallet = UserWalletBuilder(scanResponse).build()
userWalletsListManager.save(userWallet)
@ -247,7 +268,7 @@ class DetailsMiddleware {
Timber.e(error, "Wallet saving failed")
}
.doOnSuccess {
preferencesStorage.shouldShowSaveWallet = false
preferencesStorage.shouldShowSaveUserWalletScreen = false
preferencesStorage.shouldSaveUserWallets = true
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
@ -274,8 +295,11 @@ class DetailsMiddleware {
}
}
private fun saveAccessCodes() {
private fun saveAccessCodes(state: DetailsState) {
preferencesStorage.shouldSaveAccessCodes = true
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = state.scanResponse?.card?.isAccessCodeSet == true,
)
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveAccessCode,
@ -288,6 +312,9 @@ class DetailsMiddleware {
tangemSdkManager.clearSavedUserCodes()
.doOnSuccess {
preferencesStorage.shouldSaveAccessCodes = false
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = false,
)
store.dispatchOnMain(
DetailsAction.AppSettings.SwitchPrivacySetting.Success(
setting = PrivacySetting.SaveAccessCode,

View file

@ -157,15 +157,17 @@ private fun handlePrivacyAction(
state: DetailsState,
): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state
is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> when (action.setting) {
PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable)
PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable)
}
is DetailsAction.AppSettings.EnrollBiometrics -> state.copy(needEnrollBiometrics = true)
is DetailsAction.AppSettings.EnrollBiometrics.Enroll,
is DetailsAction.AppSettings.EnrollBiometrics.Cancel,
-> state.copy(needEnrollBiometrics = false)
is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy(
needEnrollBiometrics = action.needEnrollBiometrics,
)
is DetailsAction.AppSettings.SwitchPrivacySetting,
is DetailsAction.AppSettings.EnrollBiometrics,
is DetailsAction.AppSettings.CheckBiometricsStatus,
-> state
}
}

View file

@ -9,11 +9,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
@ -24,8 +25,9 @@ class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
viewModel.checkBiometricsStatus()
}
override fun onCreateView(
@ -36,7 +38,7 @@ class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
TangemTheme {
AppSettingsScreen(
state = screenState.value,
onBackPressed = {
@ -58,6 +60,11 @@ class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
}
}
override fun onResume() {
super.onResume()
viewModel.refreshBiometricsStatus()
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)

View file

@ -1,24 +1,31 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.AlertDialog
import androidx.compose.material.MaterialTheme
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.dialogs.EnrollBiometricsDialogContent
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW32
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.redux.PrivacySetting
import com.tangem.tap.features.details.ui.appsettings.components.EnrollBiometricsCard
import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.tap.features.details.ui.common.TangemSwitch
import com.tangem.wallet.R
@ -51,47 +58,43 @@ private fun AppSettings(
element = it,
onDialogStateChange = onDialogStateChange,
onSettingToggled = state.onSettingToggled,
modifier = modifier,
)
}
EnrollBiometricsDialog(dialog = state.enrollBiometricsDialog)
Column(
modifier = modifier
.fillMaxSize(),
) {
if (state.showEnrollBiometricsCard) {
EnrollBiometricsCard(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing8)
.fillMaxWidth(),
onClick = state.onEnrollBiometrics,
)
SpacerH24()
}
AppSettingsElement(
state = state,
setting = PrivacySetting.SaveWallets,
onDialogStateChange = onDialogStateChange,
modifier = modifier,
)
Spacer(modifier = Modifier.size(32.dp))
SpacerH32()
AppSettingsElement(
state = state,
setting = PrivacySetting.SaveAccessCode,
onDialogStateChange = onDialogStateChange,
modifier = modifier,
)
}
}
@Composable
private fun EnrollBiometricsDialog(
modifier: Modifier = Modifier,
dialog: EnrollBiometricsDialog?,
) {
if (dialog == null) return
EnrollBiometricsDialogContent(modifier, dialog)
}
@Composable
private fun AppSettingsElement(
modifier: Modifier = Modifier,
state: AppSettingsScreenState,
setting: PrivacySetting,
onDialogStateChange: (PrivacySetting?) -> Unit,
modifier: Modifier = Modifier,
) {
val titleRes = when (setting) {
PrivacySetting.SaveWallets -> R.string.app_settings_saved_wallet
@ -103,48 +106,63 @@ private fun AppSettingsElement(
}
val checked = state.settings[setting] ?: false
val titleTextColor by rememberUpdatedState(
newValue = if (state.isTogglesEnabled) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.secondary
},
)
val descriptionTextColor by rememberUpdatedState(
newValue = if (state.isTogglesEnabled) {
TangemTheme.colors.text.secondary
} else {
TangemTheme.colors.text.tertiary
},
)
Row(
modifier = modifier
.fillMaxWidth()
.padding(start = 20.dp),
.padding(horizontal = TangemTheme.dimens.spacing20),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(
modifier = Modifier.weight(weight = .9f),
verticalArrangement = Arrangement.Center,
modifier = modifier
.weight(0.6f)
.padding(end = 4.dp),
) {
Text(
text = stringResource(id = titleRes),
style = TangemTypography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
style = TangemTheme.typography.subtitle1,
color = titleTextColor,
)
Spacer(modifier = Modifier.size(4.dp))
SpacerH4()
Text(
text = stringResource(id = subtitleRes),
style = TangemTypography.body2,
color = colorResource(id = R.color.text_secondary),
style = TangemTheme.typography.body2,
color = descriptionTextColor,
)
}
SpacerW32()
TangemSwitch(
checked = checked,
onCheckedChange = {
enabled = state.isTogglesEnabled,
onCheckedChange = { isChecked ->
onCheckedChange(
element = setting,
enabled = it,
enabled = isChecked,
onSettingToggled = state.onSettingToggled,
onDialogStateChange = onDialogStateChange,
)
},
modifier = modifier
.padding(20.dp),
)
}
}
private fun onCheckedChange(
element: PrivacySetting, enabled: Boolean,
element: PrivacySetting,
enabled: Boolean,
onSettingToggled: (PrivacySetting, Boolean) -> Unit,
onDialogStateChange: (PrivacySetting?) -> Unit,
) {
@ -152,82 +170,88 @@ private fun onCheckedChange(
if (!enabled) {
onDialogStateChange(element)
} else {
onSettingToggled(element, enabled)
onSettingToggled(element, true)
}
}
// region Preview
@Composable
private fun SettingsAlertDialog(
element: PrivacySetting,
onDialogStateChange: (PrivacySetting?) -> Unit,
onSettingToggled: (PrivacySetting, Boolean) -> Unit,
private fun AppSettingsScreenSample(
modifier: Modifier = Modifier,
) {
val text = when (element) {
PrivacySetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message
PrivacySetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
) {
AppSettingsScreen(
state = AppSettingsScreenState(
settings = mapOf(
PrivacySetting.SaveWallets to true,
PrivacySetting.SaveAccessCode to false,
),
showEnrollBiometricsCard = false,
isTogglesEnabled = true,
onSettingToggled = { _, _ -> },
onEnrollBiometrics = {},
),
onBackPressed = { },
)
}
AlertDialog(
onDismissRequest = { onDialogStateChange(null) },
confirmButton = {
TextButton(
onClick = {
onDialogStateChange(null)
},
) {
Text(
text = stringResource(id = R.string.common_cancel),
color = colorResource(id = R.color.text_secondary),
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
)
}
},
dismissButton = {
TextButton(
onClick = {
onDialogStateChange(null)
onSettingToggled(element, false)
},
modifier = modifier.padding(bottom = 14.dp),
) {
Text(
text = stringResource(id = R.string.common_delete),
color = colorResource(id = R.color.text_warning),
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
)
}
},
title = {
Text(
text = stringResource(id = R.string.common_attention),
color = colorResource(id = R.color.text_primary_1),
)
},
text = {
Text(
text = stringResource(id = text),
color = colorResource(id = R.color.text_secondary),
)
},
shape = MaterialTheme.shapes.medium.copy(all = CornerSize(size = 28.dp)),
modifier = modifier.padding(vertical = 34.dp, horizontal = 24.dp),
)
}
@Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
fun AppSettingsScreenPreview() {
AppSettingsScreen(
state = AppSettingsScreenState(
settings = mapOf(
PrivacySetting.SaveWallets to true,
PrivacySetting.SaveAccessCode to false,
private fun AppSettingsScreenPreview_Light() {
TangemTheme {
AppSettingsScreenSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun AppSettingsScreenPreview_Dark() {
TangemTheme(isDark = true) {
AppSettingsScreenSample()
}
}
@Composable
private fun AppSettingsScreen_EnrollBiometrics_Sample(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
) {
AppSettingsScreen(
state = AppSettingsScreenState(
settings = mapOf(
PrivacySetting.SaveWallets to true,
PrivacySetting.SaveAccessCode to false,
),
showEnrollBiometricsCard = true,
isTogglesEnabled = false,
onSettingToggled = { _, _ -> },
onEnrollBiometrics = {},
),
onSettingToggled = { _, _ -> },
enrollBiometricsDialog = null,
),
onBackPressed = { },
)
}
onBackPressed = { },
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun AppSettingsScreen_EnrollBiometrics_Preview_Light() {
TangemTheme {
AppSettingsScreen_EnrollBiometrics_Sample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun AppSettingsScreen_EnrollBiometrics_Preview_Dark() {
TangemTheme(isDark = true) {
AppSettingsScreen_EnrollBiometrics_Sample()
}
}
// endregion Preview

View file

@ -1,10 +1,11 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.tap.features.details.redux.PrivacySetting
data class AppSettingsScreenState(
val settings: Map<PrivacySetting, Boolean>,
val enrollBiometricsDialog: EnrollBiometricsDialog?,
val onSettingToggled: (PrivacySetting, Boolean) -> Unit,
val settings: Map<PrivacySetting, Boolean> = emptyMap(),
val showEnrollBiometricsCard: Boolean = false,
val isTogglesEnabled: Boolean = true,
val onSettingToggled: (PrivacySetting, Boolean) -> Unit = { _, _ -> /* no-op */ },
val onEnrollBiometrics: () -> Unit = { /* no-op */ },
)

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
@ -20,8 +19,14 @@ class AppSettingsViewModel(private val store: Store<AppState>) {
PrivacySetting.SaveWallets to state.saveWallets,
PrivacySetting.SaveAccessCode to state.saveAccessCodes,
),
enrollBiometricsDialog = if (state.needEnrollBiometrics) createEnrollBiometricsDialog() else null,
onSettingToggled = { privacySetting, enabled -> onSettingsToggled(privacySetting, enabled) },
showEnrollBiometricsCard = state.needEnrollBiometrics,
isTogglesEnabled = !state.needEnrollBiometrics,
onSettingToggled = { privacySetting, enabled ->
onSettingsToggled(privacySetting, enabled)
},
onEnrollBiometrics = {
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics)
},
)
}
@ -29,12 +34,11 @@ class AppSettingsViewModel(private val store: Store<AppState>) {
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
}
private fun createEnrollBiometricsDialog() = EnrollBiometricsDialog(
onCancel = {
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics.Cancel)
},
onEnroll = {
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics.Enroll)
},
)
fun checkBiometricsStatus() {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = false))
}
fun refreshBiometricsStatus() {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = true))
}
}

View file

@ -0,0 +1,92 @@
package com.tangem.tap.features.details.ui.appsettings.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW16
import com.tangem.core.ui.res.TangemTheme
import com.tangem.wallet.R
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun EnrollBiometricsCard(
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Surface(
modifier = modifier,
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersLarge,
onClick = onClick,
) {
Row(
modifier = Modifier.padding(all = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Icon(
painter = painterResource(id = R.drawable.ic_alert_circle_24),
tint = TangemTheme.colors.icon.attention,
contentDescription = null,
)
SpacerW16()
Column {
Text(
text = stringResource(id = R.string.app_settings_enable_biometrics_title),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
SpacerH4()
Text(
text = stringResource(id = R.string.app_settings_enable_biometrics_description),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
}
// region Preview
@Composable
private fun EnrollBiometricsCardSample(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.secondary),
) {
EnrollBiometricsCard(onClick = {})
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun EnrollBiometricsCardPreview_Light() {
TangemTheme {
EnrollBiometricsCardSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun EnrollBiometricsCardPreview_Dark() {
TangemTheme(isDark = true) {
EnrollBiometricsCardSample()
}
}
// endregion Preview

View file

@ -0,0 +1,101 @@
package com.tangem.tap.features.details.ui.appsettings.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.AlertDialog
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.TextButton
import com.tangem.core.ui.components.WarningTextButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.redux.PrivacySetting
import com.tangem.wallet.R
@Composable
internal fun SettingsAlertDialog(
element: PrivacySetting,
onDialogStateChange: (PrivacySetting?) -> Unit,
onSettingToggled: (PrivacySetting, Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
val text = when (element) {
PrivacySetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message
PrivacySetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message
}
AlertDialog(
onDismissRequest = { onDialogStateChange(null) },
confirmButton = {
TextButton(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
text = stringResource(id = R.string.common_cancel),
onClick = {
onDialogStateChange(null)
},
)
},
dismissButton = {
WarningTextButton(
text = stringResource(id = R.string.common_delete),
onClick = {
onDialogStateChange(null)
onSettingToggled(element, false)
},
)
},
title = {
Text(
text = stringResource(id = R.string.common_attention),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2,
)
},
text = {
Text(
text = stringResource(id = text),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
)
},
shape = TangemTheme.shapes.roundedCornersLarge,
modifier = modifier,
)
}
// region Preview
@Composable
private fun SettingsAlertDialogSample(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
) {
SettingsAlertDialog(
element = PrivacySetting.SaveAccessCode,
onDialogStateChange = {},
onSettingToggled = { _, _ -> },
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SettingsAlertDialogPreview_Light() {
TangemTheme {
SettingsAlertDialogSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SettingsAlertDialogPreview_Dark() {
TangemTheme(isDark = true) {
SettingsAlertDialogSample()
}
}
// endregion Preview

View file

@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
@ -39,7 +39,7 @@ class CardSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
TangemTheme {
CardSettingsScreen(
state = screenState.value,
onBackPressed = {

View file

@ -15,6 +15,8 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
@ -23,6 +25,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
@ -34,14 +37,22 @@ fun CardSettingsScreen(
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
val needReadCard = state.cardDetails == null
val backgroundColor by rememberUpdatedState(
newValue = if (needReadCard) TangemTheme.colors.background.primary
else TangemTheme.colors.background.secondary,
)
SettingsScreensScaffold(
content =
if (state.cardDetails == null) {
{ CardSettingsReadCard(state.onScanCardClick, modifier = modifier) }
} else {
{ CardSettings(state = state, modifier = modifier) }
content = {
if (needReadCard) {
CardSettingsReadCard(state.onScanCardClick, modifier = modifier)
} else {
CardSettings(state = state, modifier = modifier)
}
},
titleRes = R.string.card_settings_title,
backgroundColor = backgroundColor,
onBackClick = onBackPressed,
)
}

View file

@ -25,18 +25,19 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.wallet.R
@Composable
fun SettingsScreensScaffold(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
background: @Composable (() -> Unit)? = null,
fab: @Composable (() -> Unit)? = null,
backgroundColor: Color = colorResource(id = R.color.background_primary),
backgroundColor: Color = TangemTheme.colors.background.secondary,
titleRes: Int? = null,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(true, onBackClick)
@ -58,9 +59,13 @@ fun SettingsScreensScaffold(
Column(modifier = modifier.fillMaxWidth()) {
Text(
text = stringResource(id = titleRes),
modifier = modifier.padding(start = 20.dp, end = 20.dp, bottom = 52.dp),
style = TangemTypography.headline1,
color = colorResource(id = R.color.text_primary_1),
modifier = modifier.padding(
start = TangemTheme.dimens.spacing20,
end = TangemTheme.dimens.spacing20,
bottom = TangemTheme.dimens.spacing54,
),
style = TangemTheme.typography.h1,
color = TangemTheme.colors.text.primary1,
)
content()
}
@ -86,18 +91,20 @@ fun ScreenTitle(
@Composable
fun EmptyTopBarWithNavigation(
onBackClick: () -> Unit,
backgroundColor: Color = colorResource(id = R.color.background_primary),
modifier: Modifier = Modifier,
onBackClick: () -> Unit,
backgroundColor: Color = TangemTheme.colors.background.primary,
) {
TopAppBar(
modifier = modifier,
title = { },
navigationIcon =
{
IconButton(onClick = onBackClick) {
Icon(
painterResource(id = R.drawable.ic_back), "",
tint = colorResource(id = R.color.icon_primary_1),
painter = painterResource(id = R.drawable.ic_back),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
}
},
@ -108,10 +115,10 @@ fun EmptyTopBarWithNavigation(
@Composable
fun DetailsMainButton(
modifier: Modifier = Modifier,
title: String,
enabled: Boolean = true,
onClick: (() -> Unit),
modifier: Modifier = Modifier,
) {
Button(
onClick = onClick,

View file

@ -33,10 +33,11 @@ import com.tangem.wallet.R
@Composable
fun TangemSwitch(
modifier: Modifier = Modifier,
enabledColor: Color = colorResource(id = R.color.control_checked),
disabledColor: Color = colorResource(id = R.color.icon_informative),
checkedColor: Color = colorResource(id = R.color.control_checked),
uncheckedColor: Color = colorResource(id = R.color.icon_informative),
size: Dp = 48.dp,
checked: Boolean = false,
enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit,
) {
val transition = updateTransition(checked, label = "SwitchState")
@ -45,8 +46,8 @@ fun TangemSwitch(
tween(durationMillis = 200, easing = FastOutLinearInEasing)
},
label = "",
) { enabled ->
if (enabled) enabledColor else disabledColor
) { isChecked ->
if (isChecked) checkedColor else uncheckedColor
}
val interactionSource = remember { MutableInteractionSource() }
@ -55,6 +56,7 @@ fun TangemSwitch(
.clickable(
interactionSource = interactionSource,
indication = null,
enabled = enabled,
) {
onCheckedChange(!checked)
}

View file

@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -40,7 +40,7 @@ class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
TangemTheme {
DetailsScreen(
state = detailsScreenState.value,
onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) },

View file

@ -30,7 +30,7 @@ enum class SettingsElement(
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App
TermsOfUse(R.drawable.ic_text, R.string.details_row_title_card_tou), // Terms of Use for S2C cards only
PrivacyPolicy(R.drawable.ic_lock, R.string.details_row_privacy_policy);
PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy);
}
@Immutable

View file

@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
@ -38,7 +38,7 @@ class ResetCardFragment : Fragment(), StoreSubscriber<DetailsState> {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
TangemTheme {
ResetCardScreen(
state = screenState.value,
onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) },

View file

@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
@ -38,7 +38,7 @@ class SecurityModeFragment : Fragment(), StoreSubscriber<DetailsState> {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
TangemTheme {
SecurityModeScreen(
state = screenState.value,
onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) },

View file

@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
@ -36,7 +36,7 @@ class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
AppCompatTheme {
TangemTheme {
WalletConnectScreen(
state = screenState.value,
onBackPressed = {

View file

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@ -42,6 +43,10 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
return ComposeView(inflater.context).apply {
setContent {
BackHandler {
requireActivity().finish()
}
AppCompatTheme {
ScreenContent()
}

View file

@ -22,6 +22,7 @@ import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -71,8 +72,10 @@ private fun handleHomeAction(action: Action) {
private fun readCard() = scope.launch {
delay(timeMillis = 200)
ScanCardProcessor.scan(
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
)
ScanCardProcessor.scan(
onProgressStateChange = { showProgress ->
if (showProgress) {
changeButtonState(ButtonState.PROGRESS)

View file

@ -11,7 +11,6 @@ import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -56,8 +55,7 @@ class OnboardingHelper {
backupCardsIds: List<String>? = null,
) {
when {
userWalletsListManager.hasSavedUserWallets -> scope.launch {
delay(timeMillis = 1_200)
preferencesStorage.shouldSaveUserWallets -> scope.launch {
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
@ -68,7 +66,7 @@ class OnboardingHelper {
store.dispatchOnMain(SaveWalletAction.Save)
}
tangemSdkManager.canUseBiometry &&
preferencesStorage.shouldShowSaveWallet -> scope.launch {
preferencesStorage.shouldShowSaveUserWalletScreen -> scope.launch {
delay(timeMillis = 1_200)
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(

View file

@ -12,13 +12,13 @@ import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.successOr
import com.tangem.domain.common.util.UserWalletId
import com.tangem.network.api.paymentology.AttestationResponse
import com.tangem.network.api.paymentology.PaymentologyApiService
import com.tangem.network.api.paymentology.RegisterKYCRequest
import com.tangem.network.api.paymentology.RegisterWalletRequest
import com.tangem.network.api.paymentology.RegisterWalletResponse
import com.tangem.network.api.paymentology.RegistrationResponse
import com.tangem.network.api.paymentology.tryExtractError
import com.tangem.datasource.api.paymentology.AttestationResponse
import com.tangem.datasource.api.paymentology.PaymentologyApiService
import com.tangem.datasource.api.paymentology.RegisterKYCRequest
import com.tangem.datasource.api.paymentology.RegisterWalletRequest
import com.tangem.datasource.api.paymentology.RegisterWalletResponse
import com.tangem.datasource.api.paymentology.RegistrationResponse
import com.tangem.datasource.api.paymentology.tryExtractError
import com.tangem.operations.attestation.AttestWalletKeyResponse
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.domain.getFirstToken
@ -152,12 +152,13 @@ class SaltPayActivationManager(
)
}
private fun makeRegisterKYCRequest(): RegisterKYCRequest = RegisterKYCRequest(
cardId = cardId,
publicKey = cardPublicKey,
kycProvider = "UTORG",
kycRefId = kycUrlProvider.kycRefId,
)
private fun makeRegisterKYCRequest(): RegisterKYCRequest =
RegisterKYCRequest(
cardId = cardId,
publicKey = cardPublicKey,
kycProvider = "UTORG",
kycRefId = kycUrlProvider.kycRefId,
)
companion object {
fun stub(): SaltPayActivationManager = SaltPayActivationManager(

View file

@ -8,8 +8,8 @@ import com.tangem.common.extensions.guard
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.successOr
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.network.api.paymentology.KYCStatus
import com.tangem.network.api.paymentology.RegistrationResponse
import com.tangem.datasource.api.paymentology.KYCStatus
import com.tangem.datasource.api.paymentology.RegistrationResponse
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification

View file

@ -6,7 +6,7 @@ import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.extensions.guard
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.ScanResponse
import com.tangem.network.api.paymentology.PaymentologyApiService
import com.tangem.datasource.api.paymentology.PaymentologyApiService
import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.extensions.makeSaltPayWalletManager
import com.tangem.tap.features.onboarding.products.wallet.saltPay.GnosisRegistrator

View file

@ -52,13 +52,23 @@ internal class SaveWalletMiddleware {
}
private fun saveWalletIfBiometricsEnrolled(state: SaveWalletState) {
if (tangemSdkManager.canEnrollBiometrics) {
if (tangemSdkManager.needEnrollBiometrics) {
store.dispatchOnMain(SaveWalletAction.EnrollBiometrics)
} else {
saveWallet(state)
}
}
/**
* or from [SaveWalletState.backupInfo] if provided from
* [com.tangem.tap.features.onboarding.OnboardingHelper.trySaveWalletAndNavigateToWalletScreen]
*
* If saved user's wallet was selected then pop back to [AppScreen.Wallet]
* or navigate to [AppScreen.WalletSelector] otherwise
*
* TODO: Update that logic after onboarding and backup features refactoring
* */
private fun saveWallet(state: SaveWalletState) {
val scanResponse = state.backupInfo?.scanResponse
?: store.state.globalState.scanResponse
@ -69,32 +79,37 @@ internal class SaveWalletMiddleware {
.backupCardsIds(state.backupInfo?.backupCardsIds)
.build()
val isFirstSavedWallet = !userWalletsListManager.hasSavedUserWallets
saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet)
.flatMap { userWalletsListManager.save(userWallet) }
.flatMap { userWalletsListManager.save(userWallet, canOverride = true) }
.doOnFailure { error ->
store.dispatchOnMain(SaveWalletAction.Save.Error(error))
}
.doOnSuccess {
preferencesStorage.shouldSaveUserWallets = true
preferencesStorage.shouldSaveAccessCodes = true
// Enable saving access codes only if this is the first time user save the wallet
preferencesStorage.shouldSaveAccessCodes = isFirstSavedWallet ||
preferencesStorage.shouldSaveAccessCodes
val isSavedWalletSelected =
userWalletsListManager.selectedUserWalletSync?.walletId == userWallet.walletId
store.dispatchOnMain(SaveWalletAction.Save.Success)
if (isSavedWalletSelected) {
store.dispatchOnMain(NavigationAction.PopBackTo())
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(userWallet)
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector))
}
store.dispatchOnMain(SaveWalletAction.Save.Success)
store.onUserWalletSelected(userWallet)
}
}
}
private fun saveWalletWasShown() {
preferencesStorage.shouldShowSaveWallet = false
preferencesStorage.shouldShowSaveUserWalletScreen = false
}
private suspend fun saveAccessCodeIfNeeded(

View file

@ -6,19 +6,25 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.core.ui.components.dialogs.EnrollBiometricsDialogContent
import com.tangem.core.ui.fragments.ComposeBottomSheetFragment
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.saveWallet.ui.components.EnrollBiometricsDialogContent
import com.tangem.tap.features.saveWallet.ui.components.SaveWalletScreenContent
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment<SaveWalletScreenState>() {
override val expandedHeightFraction: Float = .95f
override val expandedHeightFraction: Float = .98f
private val viewModel by viewModels<SaveWalletViewModel>()

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.saveWallet.ui
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
@Immutable
internal data class SaveWalletScreenState(

View file

@ -1,11 +1,11 @@
package com.tangem.tap.features.saveWallet.ui
import androidx.lifecycle.ViewModel
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
import com.tangem.tap.store
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

View file

@ -1,4 +1,4 @@
package com.tangem.core.ui.components.dialogs
package com.tangem.tap.features.saveWallet.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@ -17,8 +17,8 @@ import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.TextButton
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
@Composable
fun EnrollBiometricsDialogContent(

View file

@ -1,27 +1,32 @@
package com.tangem.tap.features.saveWallet.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerHHalf
import com.tangem.core.ui.components.SpacerW24
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.res.TangemTheme
import com.tangem.wallet.R
@ -34,79 +39,132 @@ internal fun SaveWalletScreenContent(
onCloseClick: () -> Unit,
) {
Column(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.spacing16),
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Hand()
IconButton(
Header(
modifier = Modifier.fillMaxWidth(),
onCloseClick = onCloseClick,
)
SpacerHHalf()
Title(
modifier = Modifier
.align(Alignment.End)
.padding(all = TangemTheme.dimens.spacing8)
.size(TangemTheme.dimens.size32),
onClick = onCloseClick,
.widthIn(max = TangemTheme.dimens.size200),
)
SpacerH32()
Description(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing34,
end = TangemTheme.dimens.spacing56,
)
.fillMaxWidth(),
)
SpacerHHalf()
Footer(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
showProgress = showProgress,
onSaveWalletClick = onSaveWalletClick,
)
SpacerH16()
}
}
@Composable
private fun Header(
modifier: Modifier = Modifier,
onCloseClick: () -> Unit,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
Hand()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_close),
tint = TangemTheme.colors.icon.secondary,
contentDescription = stringResource(id = R.string.common_cancel),
)
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onCloseClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_close),
tint = TangemTheme.colors.icon.secondary,
contentDescription = stringResource(id = R.string.common_cancel),
)
}
SpacerW8()
}
Spacer(modifier = Modifier.weight(.7f))
}
}
@Composable
private fun Title(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing32),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size56),
painter = painterResource(id = R.drawable.ic_fingerprint_24),
tint = TangemTheme.colors.icon.informative,
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
SpacerH24()
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.text.accent.copy(alpha = .12f),
shape = TangemTheme.shapes.roundedCornersMedium,
),
) {
Text(
modifier = Modifier
.padding(
vertical = TangemTheme.dimens.spacing4,
horizontal = TangemTheme.dimens.spacing12,
),
text = stringResource(R.string.save_user_wallet_agreement_new_feature),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.accent,
textAlign = TextAlign.Center,
)
}
SpacerH24()
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.onboarding_navbar_save_wallet),
text = stringResource(id = R.string.save_user_wallet_agreement_header_biometrics),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH12()
Text(
modifier = Modifier.fillMaxWidth(fraction = .87f),
text = stringResource(R.string.save_user_wallet_agreement_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
}
}
@Composable
private fun Description(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
horizontalAlignment = Alignment.Start,
) {
DescriptionItem(
iconPainter = painterResource(id = R.drawable.ic_face_recognition_24),
title = stringResource(id = R.string.save_user_wallet_agreement_access_title),
description = stringResource(id = R.string.save_user_wallet_agreement_access_description),
)
Spacer(modifier = Modifier.weight(1f))
DescriptionItem(
iconPainter = painterResource(id = R.drawable.ic_lock_24),
title = stringResource(id = R.string.save_user_wallet_agreement_code_title),
description = stringResource(id = R.string.save_user_wallet_agreement_code_description_biometrics),
)
}
}
@Composable
private fun Footer(
modifier: Modifier = Modifier,
showProgress: Boolean,
onSaveWalletClick: () -> Unit,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
showProgress = showProgress,
text = stringResource(
id = R.string.save_user_wallet_agreement_allow,
stringResource(id = R.string.common_biometric_authentication),
),
text = stringResource(id = R.string.save_user_wallet_agreement_allow_biometrics),
onClick = onSaveWalletClick,
)
SpacerH16()
Text(
modifier = Modifier.fillMaxWidth(fraction = .7f),
text = stringResource(R.string.save_user_wallet_agreement_notice),
@ -114,7 +172,41 @@ internal fun SaveWalletScreenContent(
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
SpacerH16()
}
}
@Composable
private fun DescriptionItem(
modifier: Modifier = Modifier,
iconPainter: Painter,
title: String,
description: String,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = iconPainter,
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
SpacerW24()
Column {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
SpacerH4()
Text(
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.core.ui.models
package com.tangem.tap.features.saveWallet.ui.models
import androidx.compose.runtime.Immutable

View file

@ -35,7 +35,6 @@ import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -206,7 +205,6 @@ class TokensMiddleware {
val result = tangemSdkManager.derivePublicKeys(
cardId = card.cardId,
derivations = derivations,
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && card.isAccessCodeSet,
)
when (result) {
is CompletionResult.Success -> {
@ -288,7 +286,7 @@ class TokensMiddleware {
)
scope.launch {
userWalletsListManager.update(updatedUserWallet)
userWalletsListManager.save(updatedUserWallet, canOverride = true)
.flatMap {
walletCurrenciesManager.addCurrencies(
userWallet = updatedUserWallet,

View file

@ -5,7 +5,7 @@ import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.network.api.tangemTech.TokenResponse
import com.tangem.datasource.api.tangemTech.TokenResponse
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain

View file

@ -1,7 +1,11 @@
package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.entities.FiatCurrency
@ -20,7 +24,7 @@ import java.math.BigDecimal
sealed class WalletAction : Action {
data class ResetState(val newCardId: String) : WalletAction()
data class ResetState(val newCard: CardDTO) : WalletAction()
data class SetIfTestnetCard(val isTestnet: Boolean) : WalletAction()

View file

@ -14,7 +14,11 @@ import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.*
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount
import com.tangem.tap.features.wallet.redux.reducers.findProgressState
import com.tangem.tap.features.wallet.ui.BalanceStatus
@ -43,6 +47,7 @@ data class WalletState(
val showBackupWarning: Boolean = false,
val missingDerivations: List<BlockchainNetwork> = emptyList(),
val loadingUserTokens: Boolean = false,
val walletCardsCount: Int? = null,
) : StateType {
// if you do not delegate - the application crashes on startup,

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.CurrenciesResponse
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen

View file

@ -29,7 +29,6 @@ import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.redux.reducers.toWallet
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userTokensRepository
@ -183,8 +182,6 @@ class MultiWalletMiddleware {
state: WalletState?,
) = scope.launch {
ScanCardProcessor.scan(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
selectedWallet.scanResponse.card.isAccessCodeSet,
cardId = selectedWallet.cardId,
additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain },
) { scanResponse ->
@ -192,7 +189,7 @@ class MultiWalletMiddleware {
scanResponse = scanResponse,
)
userWalletsListManager.update(userWallet)
userWalletsListManager.save(userWallet, canOverride = true)
.doOnSuccess {
store.state.globalState.tapWalletManager.loadData(userWallet, refresh = true)
}

View file

@ -294,28 +294,29 @@ class WalletMiddleware {
}
is WalletAction.UserWalletChanged -> Unit
is WalletAction.WalletStoresChanged -> {
scope.launch(Dispatchers.Default) {
fetchTotalFiatBalance(action.walletStores, walletState)
findMissedDerivations(action.walletStores)
tryToShowAppRatingWarning(action.walletStores)
}
fetchTotalFiatBalance(action.walletStores, walletState)
findMissedDerivations(action.walletStores)
tryToShowAppRatingWarning(action.walletStores)
}
is WalletAction.TotalFiatBalanceChanged -> Unit
}
}
private fun fetchTotalFiatBalance(walletStores: List<WalletStoreModel>, state: WalletState) {
scope.launch {
val totalFiatBalance = totalFiatBalanceCalculator.calculate(
prevAmount = state.totalBalance?.fiatAmount ?: BigDecimal.ZERO,
scope.launch(Dispatchers.Default) {
val totalFiatBalance = totalFiatBalanceCalculator.calculateOrNull(
prevAmount = state.totalBalance?.fiatAmount,
walletStores = walletStores,
)
store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance))
if (totalFiatBalance != null) {
store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance))
}
}
}
private fun findMissedDerivations(wallStores: List<WalletStoreModel>) {
scope.launch {
scope.launch(Dispatchers.Default) {
val missedDerivations = wallStores
.filter { store ->
store.walletsData.any { it.status is WalletDataModel.MissedDerivation }
@ -327,15 +328,17 @@ class WalletMiddleware {
}
private fun tryToShowAppRatingWarning(walletStores: List<WalletStoreModel>) {
warningsMiddleware.tryToShowAppRatingWarning(
hasNonZeroWallets = walletStores
.flatMap { it.walletsData }
.any { it.status.amount.isGreaterThan(BigDecimal.ZERO) },
)
scope.launch(Dispatchers.Default) {
warningsMiddleware.tryToShowAppRatingWarning(
hasNonZeroWallets = walletStores
.flatMap { it.walletsData }
.any { it.status.amount.isGreaterThan(BigDecimal.ZERO) },
)
}
}
private fun showSaveWalletIfNeeded() {
if (preferencesStorage.shouldShowSaveWallet
if (preferencesStorage.shouldShowSaveUserWalletScreen
&& tangemSdkManager.canUseBiometry
&& store.state.navigationState.backStack.lastOrNull() == AppScreen.Wallet
) {

View file

@ -53,7 +53,12 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
is WalletAction.Warnings -> newState = handleCheckSignedHashesActions(action, newState)
is WalletAction.MultiWallet -> newState = multiWalletReducer.reduce(action, newState)
is WalletAction.ResetState -> newState = WalletState(cardId = action.newCardId)
is WalletAction.ResetState -> {
newState = WalletState(
cardId = action.newCard.cardId,
walletCardsCount = action.newCard.findCardsCount(),
)
}
is WalletAction.SetIfTestnetCard -> newState = newState.copy(isTestnet = action.isTestnet)
is WalletAction.EmptyWallet -> {
newState = newState.copy(
@ -333,6 +338,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
showBackupWarning = card.isMultiwalletAllowed &&
card.settings.isBackupAllowed &&
card.backupStatus == CardDTO.BackupStatus.NoBackup,
walletCardsCount = card.findCardsCount(),
)
}
is WalletAction.WalletStoresChanged -> {
@ -366,6 +372,11 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
return newState
}
private fun CardDTO.findCardsCount(): Int? {
return (this.backupStatus as? CardDTO.BackupStatus.Active)?.cardCount?.inc()
?.takeIf { this.isMultiwalletAllowed }
}
@JvmName("walletStoreModelToReduxModel")
private fun List<WalletStoreModel>.mapToReduxModel(
isMultiWalletAllowed: Boolean,

View file

@ -3,13 +3,16 @@ package com.tangem.tap.features.wallet.ui.wallet
import android.widget.Button
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.CurrenciesRepository
@ -43,21 +46,11 @@ class MultiWalletView : WalletView() {
lAddress.root.hide()
rowButtons.hide()
lSingleWalletBalance.root.hide()
lCardTotalBalance.root.show()
rvMultiwallet.show()
btnAddToken.show()
setupWalletCardNumber(binding)
}
private fun setupWalletCardNumber(binding: FragmentWalletBinding) = with(binding) {
val card = store.state.globalState.scanResponse?.card
if (card?.backupStatus is CardDTO.BackupStatus.Active) {
val cardCount = (card.backupStatus as CardDTO.BackupStatus.Active).cardCount + 1
tvTwinCardNumber.show()
tvTwinCardNumber.text = tvTwinCardNumber.getQuantityString(R.plurals.card_label_card_count, cardCount)
} else {
tvTwinCardNumber.hide()
}
}
override fun onViewCreated() {
setupWalletsRecyclerView()
@ -79,6 +72,7 @@ class MultiWalletView : WalletView() {
handleTotalBalance(binding, state.totalBalance, state.state)
handleBackupWarning(binding, state.showBackupWarning)
handleRescanWarning(binding, state.missingDerivations.isNotEmpty())
setupWalletCardNumber(binding, state.walletCardsCount)
walletsAdapter.submitList(state.walletsData)
binding.pbLoadingUserTokens.show(state.loadingUserTokens)
@ -107,6 +101,16 @@ class MultiWalletView : WalletView() {
handleErrorStates(state = state, binding = binding, fragment = fragment)
}
private fun setupWalletCardNumber(binding: FragmentWalletBinding, walletCardsCount: Int?) = with(binding) {
if (walletCardsCount != null) {
tvTwinCardNumber.show()
tvTwinCardNumber.text =
tvTwinCardNumber.getQuantityString(R.plurals.card_label_card_count, walletCardsCount)
} else {
tvTwinCardNumber.hide()
}
}
private fun handleBackupWarning(
binding: FragmentWalletBinding,
showBackupWarning: Boolean,
@ -134,21 +138,20 @@ class MultiWalletView : WalletView() {
totalBalance: TotalBalance?,
progressState: ProgressState,
) = with(binding.lCardTotalBalance) {
root.isVisible = totalBalance != null
if (totalBalance != null) {
// Skip changes when on refreshing state
if (totalBalance.state == ProgressState.Refreshing ||
progressState == ProgressState.Refreshing
) return@with
if (totalBalance == null) {
veilBalance.animateVisibility(show = true)
root.isVisible = progressState == ProgressState.Loading
} else {
root.isVisible = true
if (totalBalance.state == ProgressState.Loading) {
veilBalance.veil()
} else {
veilBalance.unVeil()
// Skip changes when on refreshing state
if (totalBalance.state == ProgressState.Refreshing || progressState == ProgressState.Refreshing) {
return@with
}
tvProcessing.animateVisibility(
show = totalBalance.state == ProgressState.Error,
)
veilBalance.animateVisibility(show = totalBalance.state == ProgressState.Loading)
tvBalance.animateVisibility(show = totalBalance.state != ProgressState.Loading)
tvProcessing.animateVisibility(show = totalBalance.state == ProgressState.Error)
tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString(
currencySymbol = totalBalance.fiatCurrency.symbol,

View file

@ -33,6 +33,7 @@ class SingleWalletView : WalletView() {
}
private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) {
tvTwinCardNumber.hide()
rvMultiwallet.hide()
btnAddToken.hide()
rvPendingTransaction.hide()

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.model.builders.UserWalletBuilder
@ -19,6 +20,7 @@ import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.totalFiatBalanceCalculator
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletStoresManager
@ -26,6 +28,7 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
import java.math.BigDecimal
internal class WalletSelectorMiddleware {
val middleware: Middleware<AppState> = { _, appStateProvider ->
@ -127,7 +130,8 @@ internal class WalletSelectorMiddleware {
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
if (isSavedWalletSelected) {
store.dispatchOnMain(NavigationAction.PopBackTo())
updateAccessCodeRequestPolicy(selectedWallet)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedWallet)
}
}
@ -141,7 +145,8 @@ internal class WalletSelectorMiddleware {
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
}
.doOnSuccess { selectedWallet ->
store.dispatchOnMain(NavigationAction.PopBackTo())
updateAccessCodeRequestPolicy(selectedWallet)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedWallet)
}
}
@ -163,7 +168,7 @@ internal class WalletSelectorMiddleware {
scope.launch {
userWalletsListManager.get(walletId = UserWalletId(walletId))
.map { it.copy(name = newName) }
.flatMap { userWalletsListManager.update(it) }
.flatMap { userWalletsListManager.save(it, canOverride = true) }
.doOnFailure { error ->
store.dispatchOnMain(WalletSelectorAction.HandleError(error))
}
@ -174,7 +179,6 @@ internal class WalletSelectorMiddleware {
crossinline onCardScanned: suspend (ScanResponse) -> Unit,
) {
ScanCardProcessor.scan(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
onSuccess = {
onCardScanned(it)
},
@ -208,11 +212,19 @@ internal class WalletSelectorMiddleware {
val isSelectedWalletRemoved = prevSelectedWalletId != selectedWallet.walletId.stringValue
if (isSelectedWalletRemoved) {
updateAccessCodeRequestPolicy(selectedWallet)
store.onUserWalletSelected(selectedWallet)
}
}
}
private fun updateAccessCodeRequestPolicy(userWallet: UserWallet) {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
userWallet.scanResponse.card.isAccessCodeSet,
)
}
private suspend fun UserWalletModel.updateWalletStoresAndCalculateFiatBalance(
walletStores: List<WalletStoreModel>,
): UserWalletModel {
@ -221,7 +233,6 @@ internal class WalletSelectorMiddleware {
is UserWalletModel.Type.MultiCurrency -> type.copy(
tokensCount = walletStores.flatMap { it.walletsData }.size,
)
is UserWalletModel.Type.SingleCurrency -> type.copy(
blockchainName = walletStores
.firstOrNull()
@ -233,6 +244,7 @@ internal class WalletSelectorMiddleware {
fiatBalance = totalFiatBalanceCalculator.calculate(
prevAmount = fiatBalance.amount,
walletStores = walletStores,
initial = TotalFiatBalance.Loaded(BigDecimal.ZERO),
),
)
}

View file

@ -1,11 +1,23 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
@ -15,7 +27,11 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButtonIconRight
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
@ -35,31 +51,16 @@ internal fun WalletSelectorScreenContent(
onEditSelectedWalletClick: () -> Unit,
onDeleteSelectedWalletsClick: () -> Unit,
) {
val editingWalletsSize by rememberUpdatedState(newValue = state.editingWalletsIds.size)
Column(modifier = modifier) {
Hand()
SpacerH12()
if (editingWalletsSize > 0) {
EditWalletsBar(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
editingWalletsSize = editingWalletsSize,
onClearSelectedClick = onClearSelectedClick,
onEditSelectedWalletClick = onEditSelectedWalletClick,
onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick,
)
} else {
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.user_wallet_list_title),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
SpacerH12()
Header(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
editingWalletsIds = state.editingWalletsIds,
onClearSelectedClick = onClearSelectedClick,
onEditSelectedWalletClick = onEditSelectedWalletClick,
onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick,
)
Column(
modifier = Modifier
.verticalScroll(
@ -76,28 +77,15 @@ internal fun WalletSelectorScreenContent(
onWalletLongClick = onWalletLongClick,
)
SpacerH24()
if (state.isLocked) {
PrimaryButton(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(
id = R.string.user_wallet_list_unlock_all,
stringResource(id = R.string.common_biometrics),
),
showProgress = state.showUnlockProgress,
onClick = onUnlockClick,
)
SpacerH12()
}
SecondaryButtonIconRight(
Footer(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(R.string.user_wallet_list_add_button),
showProgress = state.showAddCardProgress,
icon = painterResource(id = R.drawable.ic_tangem),
onClick = onAddCardClick,
isLocked = state.isLocked,
showUnlockProgress = state.showUnlockProgress,
showAddCardProgress = state.showAddCardProgress,
onUnlockClick = onUnlockClick,
onAddCardClick = onAddCardClick,
)
SpacerH16()
}
@ -105,62 +93,42 @@ internal fun WalletSelectorScreenContent(
}
@Composable
private fun EditWalletsBar(
private fun Header(
modifier: Modifier = Modifier,
editingWalletsSize: Int,
editingWalletsIds: List<String>,
onClearSelectedClick: () -> Unit,
onEditSelectedWalletClick: () -> Unit,
onDeleteSelectedWalletsClick: () -> Unit,
) {
val showEditAction = remember(editingWalletsSize) { editingWalletsSize in 1 until 2 }
Row(
modifier = modifier
.padding(vertical = TangemTheme.dimens.spacing8)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onClearSelectedClick,
val editingWalletsSize by rememberUpdatedState(newValue = editingWalletsIds.size)
val hasEditingWallets by remember {
derivedStateOf { editingWalletsSize > 0 }
}
Column(modifier = modifier) {
Hand()
Box(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size44),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_close),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "Unselect wallets",
)
}
SpacerW16()
Text(
text = stringResource(id = R.string.user_wallet_list_editing_count, editingWalletsSize),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
SpacerWMax()
if (showEditAction) {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onEditSelectedWalletClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_pencil_outline_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "Edit wallet name",
if (hasEditingWallets) {
EditWalletsBar(
modifier = Modifier.fillMaxWidth(),
editingWalletsSize = editingWalletsSize,
onClearSelectedClick = onClearSelectedClick,
onEditSelectedWalletClick = onEditSelectedWalletClick,
onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick,
)
} else {
Text(
text = stringResource(R.string.user_wallet_list_title),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
SpacerW8()
}
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onDeleteSelectedWalletsClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_trash),
tint = TangemTheme.colors.icon.warning,
contentDescription = "Delete selected wallets",
)
}
}
}
@ -208,6 +176,101 @@ private fun WalletsList(
}
}
@Composable
private fun Footer(
modifier: Modifier = Modifier,
isLocked: Boolean,
showUnlockProgress: Boolean,
showAddCardProgress: Boolean,
onUnlockClick: () -> Unit,
onAddCardClick: () -> Unit,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
if (isLocked) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(
id = R.string.user_wallet_list_unlock_all,
stringResource(id = R.string.common_biometrics),
),
showProgress = showUnlockProgress,
onClick = onUnlockClick,
)
}
SecondaryButtonIconRight(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.user_wallet_list_add_button),
showProgress = showAddCardProgress,
icon = painterResource(id = R.drawable.ic_tangem),
onClick = onAddCardClick,
)
}
}
@Composable
private fun EditWalletsBar(
modifier: Modifier = Modifier,
editingWalletsSize: Int,
onClearSelectedClick: () -> Unit,
onEditSelectedWalletClick: () -> Unit,
onDeleteSelectedWalletsClick: () -> Unit,
) {
val showEditAction by remember(editingWalletsSize) {
derivedStateOf { editingWalletsSize in 1 until 2 }
}
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onClearSelectedClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_close),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "Unselect wallets",
)
}
Text(
text = stringResource(id = R.string.user_wallet_list_editing_count, editingWalletsSize),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
SpacerWMax()
if (showEditAction) {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onEditSelectedWalletClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_pencil_outline_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "Change wallet name",
)
}
}
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onDeleteSelectedWalletsClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_trash),
tint = TangemTheme.colors.icon.warning,
contentDescription = "Delete selected wallets",
)
}
}
}
// region Preview
@Composable
private fun WalletSelectorScreenContentSample(

View file

@ -7,8 +7,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@ -85,14 +84,15 @@ private fun WalletCardImage(
.height(TangemTheme.dimens.size32)
.clip(TangemTheme.shapes.roundedCornersSmall2)
val checkedColorFilter by rememberUpdatedState(
newValue = if (isChecked) {
val tintColor = TangemTheme.colors.icon.accent.copy(alpha = .6f)
val checkedColorFilter = remember(isChecked) {
if (isChecked) {
ColorFilter.tint(
color = TangemTheme.colors.icon.accent.copy(alpha = .6f),
color = tintColor,
blendMode = BlendMode.SrcOver,
)
} else null,
)
} else null
}
SubcomposeAsyncImage(
modifier = cardImageModifier,
@ -106,22 +106,15 @@ private fun WalletCardImage(
contentDescription = null,
)
if (isSelected) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.size(TangemTheme.dimens.size18)
.background(
color = Color.White,
shape = CircleShape,
),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.padding(all = (0.5).dp),
painter = painterResource(id = R.drawable.ic_check_circle_18),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
when {
isChecked -> {
CheckedWalletMark(
modifier = Modifier.matchParentSize(),
)
}
isSelected -> {
SelectedWalletBadge(
modifier = Modifier.align(Alignment.TopEnd),
)
}
}
@ -308,4 +301,42 @@ private fun CardImageShimmer(
.background(TangemTheme.colors.button.secondary),
)
}
}
@Composable
private fun SelectedWalletBadge(
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size18)
.background(
color = Color.White,
shape = CircleShape,
),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.padding(all = (0.5).dp),
painter = painterResource(id = R.drawable.ic_check_circle_18),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
}
@Composable
private fun CheckedWalletMark(
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_check_24),
tint = TangemTheme.colors.icon.primary2,
contentDescription = null,
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.welcome.redux
import android.content.Intent
import com.tangem.common.core.TangemError
import org.rekotlin.Action
@ -14,5 +15,7 @@ internal sealed interface WelcomeAction : Action {
data class Error(val error: TangemError) : WelcomeAction
}
data class HandleDeepLink(val intent: Intent?) : WelcomeAction
object CloseError : WelcomeAction
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.welcome.redux
import android.content.Intent
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.common.ScanResponse
@ -10,37 +11,47 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.intentHandler
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
internal class WelcomeMiddleware {
val middleware: Middleware<AppState> = { _, _ ->
val middleware: Middleware<AppState> = { _, appStateProvider ->
{ next ->
{ action ->
if (action is WelcomeAction) {
handleAction(action)
val appState = appStateProvider()
if (action is WelcomeAction && appState != null) {
handleAction(action, appState.welcomeState)
}
next(action)
}
}
}
private fun handleAction(action: WelcomeAction) {
private fun handleAction(action: WelcomeAction, state: WelcomeState) {
when (action) {
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometry()
is WelcomeAction.ProceedWithCard -> proceedWithCard()
is WelcomeAction.ProceedWithBiometrics.Error -> Unit
is WelcomeAction.ProceedWithCard.Error -> Unit
is WelcomeAction.ProceedWithBiometrics.Success -> Unit
is WelcomeAction.ProceedWithCard.Success -> Unit
is WelcomeAction.CloseError -> Unit
is WelcomeAction.ProceedWithBiometrics -> {
proceedWithBiometry(state)
}
is WelcomeAction.ProceedWithCard -> {
proceedWithCard(state)
}
is WelcomeAction.ProceedWithBiometrics.Error,
is WelcomeAction.ProceedWithCard.Error,
is WelcomeAction.ProceedWithBiometrics.Success,
is WelcomeAction.ProceedWithCard.Success,
is WelcomeAction.CloseError,
is WelcomeAction.HandleDeepLink,
-> Unit
}
}
private fun proceedWithBiometry() {
private fun proceedWithBiometry(state: WelcomeState) {
scope.launch {
userWalletsListManager.unlockWithBiometry()
.doOnFailure { error ->
@ -48,15 +59,22 @@ internal class WelcomeMiddleware {
}
.doOnSuccess { selectedUserWallet ->
if (selectedUserWallet != null) {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
selectedUserWallet.scanResponse.card.isAccessCodeSet,
)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success)
store.onUserWalletSelected(selectedUserWallet)
handleDeepLinkIfNeeded(state.deepLinkIntent)
}
}
}
}
private fun proceedWithCard() = scope.launch {
private fun proceedWithCard(state: WelcomeState) = scope.launch {
scanCardInternal { scanResponse ->
val userWallet = UserWalletBuilder(scanResponse).build()
@ -65,13 +83,22 @@ internal class WelcomeMiddleware {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error))
}
.doOnSuccess {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = false,
)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
store.onUserWalletSelected(userWallet)
handleDeepLinkIfNeeded(state.deepLinkIntent)
}
}
}
private fun handleDeepLinkIfNeeded(intent: Intent?) {
intentHandler.handleWalletConnectLink(intent)
}
private suspend inline fun scanCardInternal(
crossinline onCardScanned: suspend (ScanResponse) -> Unit,
) {

View file

@ -12,6 +12,7 @@ internal object WelcomeReducer {
private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState {
return when (action) {
is WelcomeAction.HandleDeepLink -> state.copy(deepLinkIntent = action.intent)
is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true)
is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true)
is WelcomeAction.ProceedWithBiometrics.Error -> state.copy(

View file

@ -1,10 +1,12 @@
package com.tangem.tap.features.welcome.redux
import android.content.Intent
import com.tangem.common.core.TangemError
import org.rekotlin.StateType
data class WelcomeState(
val isUnlockWithBiometricsInProgress: Boolean = false,
val isUnlockWithCardInProgress: Boolean = false,
val deepLinkIntent: Intent? = null,
val error: TangemError? = null,
) : StateType

View file

@ -1,12 +1,19 @@
package com.tangem.tap.features.welcome.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
@ -39,6 +46,10 @@ internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
setSystemBarsColor(color = backgroundColor)
}
BackHandler {
requireActivity().finish()
}
Box(
modifier = modifier
.systemBarsPadding(),

View file

@ -6,7 +6,7 @@ import com.tangem.common.extensions.calculateSha512
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.network.common.createRetrofitInstance
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager

View file

@ -6,7 +6,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.network.common.createRetrofitInstance
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.tap.common.extensions.urlEncode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.models.Currency

View file

@ -3,7 +3,7 @@ package com.tangem.tap.network.payid
import com.squareup.moshi.JsonClass
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.network.common.createRetrofitInstance
import com.tangem.datasource.api.common.createRetrofitInstance
import retrofit2.Retrofit
class PayIdService {

View file

@ -2,7 +2,7 @@ package com.tangem.tap.network.payid
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.network.common.createRetrofitInstance
import com.tangem.datasource.api.common.createRetrofitInstance
/**
[REDACTED_AUTHOR]

View file

@ -3,7 +3,7 @@ package com.tangem.tap.persistence
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.datasource.api.tangemTech.CurrenciesResponse
import com.tangem.tap.common.entities.FiatCurrency
/**

View file

@ -4,7 +4,7 @@ import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.network.common.MoshiConverter
import com.tangem.datasource.api.common.MoshiConverter
import java.util.*
class PreferencesStorage(applicationContext: Application) {
@ -33,7 +33,7 @@ class PreferencesStorage(applicationContext: Application) {
get() = preferences.getLong(CHAT_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L }
set(value) = preferences.edit { putLong(CHAT_FIRST_LAUNCH_KEY, value ?: 0) }
var shouldShowSaveWallet: Boolean
var shouldShowSaveUserWalletScreen: Boolean
get() = preferences.getBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, true)
set(value) = preferences.edit {
putBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, value)

View file

@ -27,20 +27,13 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.skydoves.androidveil.VeilLayout
android:id="@+id/veil_balance"
<FrameLayout
android:id="@+id/balance_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toTopOf="@id/tv_processing"
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">
app:layout_constraintTop_toBottomOf="@id/tv_title">
<TextView
android:id="@+id/tv_balance"
@ -51,9 +44,23 @@
android:textColor="@color/darkGray6"
android:textSize="26sp"
android:textStyle="bold"
android:visibility="gone"
tools:visibility="visible"
tools:text="22 325.40 $" />
</com.skydoves.androidveil.VeilLayout>
<com.skydoves.androidveil.VeilLayout
android:id="@+id/veil_balance"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible"
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" />
</FrameLayout>
<TextView
android:id="@+id/tv_processing"
@ -65,7 +72,7 @@
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/veil_balance"
app:layout_constraintTop_toBottomOf="@id/balance_container"
tools:visibility="visible" />
<TextView

View file

@ -42,6 +42,9 @@ dependencies {
/** Project */
implementation(project(":libs:auth"))
/** Tangem libraries */
implementation(Tangem.cardCore)
/** DI */
implementation(Library.hilt)
kapt(Library.hiltKapt)

View file

@ -1,4 +1,4 @@
package com.tangem.network.common
package com.tangem.datasource.api.common
import okhttp3.Interceptor
import okhttp3.Response
@ -7,7 +7,7 @@ import okhttp3.Response
[REDACTED_AUTHOR]
*/
open class AddHeaderInterceptor(
private val headers: Map<String, String>
private val headers: Map<String, String>,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
@ -21,10 +21,10 @@ open class AddHeaderInterceptor(
}
}
class CacheControlHttpInterceptor(maxAgeSeconds: Int) : AddHeaderInterceptor(mapOf(
"Cache-Control" to "max-age=$maxAgeSeconds",
))
class CacheControlHttpInterceptor(maxAgeSeconds: Int) : AddHeaderInterceptor(
mapOf("Cache-Control" to "max-age=$maxAgeSeconds"),
)
class CardPublicKeyHttpInterceptor(cardPublicKeyHex: String) : AddHeaderInterceptor(mapOf(
"card_public_key" to cardPublicKeyHex,
))
class CardPublicKeyHttpInterceptor(cardPublicKeyHex: String) : AddHeaderInterceptor(
mapOf("card_public_key" to cardPublicKeyHex),
)

View file

@ -1,4 +1,4 @@
package com.tangem.network.common
package com.tangem.datasource.api.common
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
@ -9,8 +9,10 @@ import retrofit2.converter.moshi.MoshiConverterFactory
/**
[REDACTED_AUTHOR]
*/
//todo needs to be refactored
object MoshiConverter {
//todo refactor: provide via DI
var INSTANCE = MoshiJsonConverter()
private set
@ -23,7 +25,9 @@ object MoshiConverter {
fun createFactory(moshi: Moshi = INSTANCE.moshi): Converter.Factory = MoshiConverterFactory.create(moshi)
//todo provide via DI using quealifiers
fun defaultMoshi(): Moshi = INSTANCE.moshi
//todo provide via DI using quealifiers
fun sdkMoshi(): Moshi = MoshiJsonConverter.INSTANCE.moshi
}

View file

@ -1,4 +1,4 @@
package com.tangem.network.common
package com.tangem.datasource.api.common
import okhttp3.Interceptor
import okhttp3.OkHttpClient

View file

@ -0,0 +1,200 @@
package com.tangem.datasource.api.oneinch
import com.tangem.datasource.api.oneinch.models.AllowanceResponse
import com.tangem.datasource.api.oneinch.models.ApproveCalldataResponse
import com.tangem.datasource.api.oneinch.models.ApproveSpenderResponse
import com.tangem.datasource.api.oneinch.models.ProtocolsResponse
import com.tangem.datasource.api.oneinch.models.QuoteResponse
import com.tangem.datasource.api.oneinch.models.StatusResponse
import com.tangem.datasource.api.oneinch.models.SwapResponse
import com.tangem.datasource.api.oneinch.models.TokensResponse
import retrofit2.http.GET
import retrofit2.http.Query
interface OneInchApi {
/**
* Healthcheck return 200 if service is available
*
* @return [StatusResponse]
*/
@GET("healthcheck")
suspend fun healthcheck(): StatusResponse
//region Approve
/**
* Address of the 1inch router that must be trusted to spend funds for the exchange
*
* @return [ApproveSpenderResponse]
*/
@GET("approve/spender")
suspend fun approveSpender(): ApproveSpenderResponse
/**
* Generate data for calling the contract in order to allow the 1inch router to spend funds
*
* @param tokenAddress Token address you want to exchange
* @param amount The number of tokens that the 1inch router is allowed to spend.
* If not specified, it will be allowed to spend an infinite amount of tokens.
*
* @return [ApproveCalldataResponse] Transaction body to allow the exchange with the 1inch router
*/
@GET("approve/transaction")
suspend fun approveTransaction(
@Query("tokenAddress") tokenAddress: String,
@Query("amount") amount: String? = null,
): ApproveCalldataResponse
/**
* Get the number of tokens that the 1inch router is allowed to spend
*
* @param tokenAddress Token address you want to exchange
* @param walletAddress Wallet address for which you want to check
*
* @return [AllowanceResponse]
*/
@GET("approve/allowance")
suspend fun approveAllowance(
@Query("tokenAddress") tokenAddress: String,
@Query("walletAddress") walletAddress: String,
): AllowanceResponse
//endregion Approve
//region Info
/**
* List of tokens that are available for swap in the 1inch Aggregation protocol
*
* @return [TokensResponse]
*/
@GET("tokens")
suspend fun tokensAvailable(): TokensResponse
/**
* List of liquidity sources that are available for routing in the 1inch Aggregation protocol
*
* @return
*/
@GET("liquidity-sources")
suspend fun liquiditySources(): ProtocolsResponse
//endregion Info
//region Swap
/**
* Find the best quote to exchange via 1inch router
*
* @param fromTokenAddress Example : 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
* @param toTokenAddress Example : 0x111111111117dc0aa78b770fa6a738034120c302
* @param amount amount of a token to sell, set in minimal divisible units e.g.:
* 1.00 DAI set as 1000000000000000000
* 51.03 USDC set as 51030000
*
* @param protocols default: all
* @param fee this percentage of fromTokenAddress token amount will be sent to referrerAddress,
* the rest will be used as input for a swap
* Min: 0; max: 3; Max: 0; max: 3; default: 0; !should be the same for quote and swap!
*
* @param gasLimit maximum amount of gas for a swap;
* @param connectorTokens token-connectors can be specified via this parameter.
* The more is set the longer route estimation will take.
* If not set, default token-connectors will be usedmax: 5; !should be the same for quote and swap!
*
* @param complexityLevel maximum number of token-connectors to be used in a transaction.
* The more is used the longer route estimation will take
* min: 0; max: 3; default: 2; !should be the same for quote and swap!
*
* @param mainRouteParts default: 10; max: 50 !should be the same for quote and swap!
* @param parts limit maximum number of parts each main route parts can be split into;
* should be the same for a quote and swap
* default: 20; max: 100
*
* @param gasPrice 1inch takes in account gas expenses to determine exchange route.
* It is important to use the same gas price on the quote and swap methods.
* Gas price set in wei: 12.5 GWEI set as 12500000000
* default: fast from network
*
* @return [QuoteResponse]
*/
suspend fun quote(
@Query("fromTokenAddress") fromTokenAddress: String,
@Query("toTokenAddress") toTokenAddress: String,
@Query("amount") amount: String,
@Query("protocols") protocols: String? = null,
@Query("fee") fee: String? = null,
@Query("gasLimit") gasLimit: String? = null,
@Query("connectorTokens") connectorTokens: String? = null,
@Query("complexityLevel") complexityLevel: String? = null,
@Query("mainRouteParts") mainRouteParts: String? = null,
@Query("parts") parts: String? = null,
@Query("gasPrice") gasPrice: String? = null,
): QuoteResponse
/**
* Generate data for calling the 1inch router for exchange
*
* @param fromTokenAddress Example : 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
* @param toTokenAddress Example : 0x111111111117dc0aa78b770fa6a738034120c302
* @param amount amount of a token to sell, set in minimal divisible units e.g.:
* 1.00 DAI set as 1000000000000000000
* 51.03 USDC set as 51030000
*
* @param fromAddress The address that calls the 1inch contract
* @param slippage limit of price slippage you are willing to accept in percentage, may be set with decimals.
* &slippage=0.5 means 0.5% slippage is acceptable. Low values increase chances that transaction will fail,
* high values increase chances of front running. min: 0; max: 50;
*
* @param protocols default: all
* @param destinationAddress Receiver of destination currency. default: fromAddress
* @param fee this percentage of fromTokenAddress token amount will be sent to referrerAddress,
* the rest will be used as input for a swap
* Min: 0; max: 3; Max: 0; max: 3; default: 0; !should be the same for quote and swap!
*
* @param permit https://eips.ethereum.org/EIPS/eip-2612
* @param compatibilityMode Allows to build calldata without optimized routers
* @param burnChi If true, CHI will be burned from fromAddress to compensate gas.
* Check CHI balance and allowance before turning that on. CHI should be approved for the spender address
*
* @param connectorTokens token-connectors can be specified via this parameter.
* The more is set the longer route estimation will take.
* If not set, default token-connectors will be usedmax: 5; !should be the same for quote and swap!
*
* @param complexityLevel maximum number of token-connectors to be used in a transaction.
* The more is used the longer route estimation will take
* min: 0; max: 3; default: 2; !should be the same for quote and swap!
*
* @param mainRouteParts default: 10; max: 50 !should be the same for quote and swap!
* @param parts limit maximum number of parts each main route parts can be split into;
* should be the same for a quote and swap
* default: 20; max: 100
*
* @param gasLimit maximum amount of gas for a swap;
* @param gasPrice 1inch takes in account gas expenses to determine exchange route.
* It is important to use the same gas price on the quote and swap methods.
* Gas price set in wei: 12.5 GWEI set as 12500000000
* default: fast from network
*
* @return [SwapResponse]
*/
suspend fun swap(
@Query("fromTokenAddress") fromTokenAddress: String,
@Query("toTokenAddress") toTokenAddress: String,
@Query("amount") amount: String,
@Query("fromAddress") fromAddress: String,
@Query("slippage") slippage: Int,
@Query("protocols") protocols: String? = null,
@Query("destReceiver") destinationAddress: String? = null,
@Query("referrerAddress") referrerAddress: String? = null,
@Query("fee") fee: String? = null,
@Query("disableEstimate") disableEstimate: Boolean? = null,
@Query("permit") permit: String? = null,
@Query("compatibilityMode") compatibilityMode: Boolean? = null,
@Query("burnChi") burnChi: Boolean? = null,
@Query("allowPartialFill") allowPartialFill: Boolean? = null,
@Query("parts") parts: String? = null,
@Query("mainRouteParts") mainRouteParts: String? = null,
@Query("connectorTokens") connectorTokens: String? = null,
@Query("complexityLevel") complexityLevel: String? = null,
@Query("gasLimit") gasLimit: String? = null,
@Query("gasPrice") gasPrice: String? = null,
): SwapResponse
//endregion Swap
}

View file

@ -0,0 +1,7 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
data class AllowanceResponse(
@Json(name = "allowance") val allowance: String,
)

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Approve calldata response
*
* @property data The encoded data to call the approve method on the swapped token contract
* @property gasPrice Gas price for fast transaction processing
* @property toAddress Token address that will be allowed to exchange through 1inch router
* @property value Native token value in WEI (for approve is always 0)
*/
data class ApproveCalldataResponse(
@Json(name = "data") val data: String,
@Json(name = "gasPrice") val gasPrice: String,
@Json(name = "to") val toAddress: String,
@Json(name = "value") val value: String,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Approve spender response
*
* @property address Address of the 1inch router that must be trusted to spend funds for the exchange
*/
data class ApproveSpenderResponse(
@Json(name = "address") val address: String,
)

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Path view dto
*/
data class PathViewDto(
@Json(name = "name") val name: String,
@Json(name = "part") val part: Int,
@Json(name = "fromTokenAddress") val fromTokenAddress: String,
@Json(name = "toTokenAddress") val toTokenAddress: String,
)

View file

@ -0,0 +1,27 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Protocols response
*
* @property protocols List of protocols that are available for routing in the 1inch Aggregation protocol
*/
data class ProtocolsResponse(
@Json(name = "protocols") val protocols: List<ProtocolImageDto>,
)
/**
* Protocol image
*
* @property id Protocol id
* @property title Protocol title
* @property image Protocol logo image
* @property imageColor Protocol logo image in color
*/
data class ProtocolImageDto(
@Json(name = "id") val id: String,
@Json(name = "title") val title: String,
@Json(name = "img") val image: String,
@Json(name = "img_color") val imageColor: String,
)

View file

@ -0,0 +1,22 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
/**
* Quote response
*
* @property fromToken Source token info
* @property toToken Destination token info
* @property toTokenAmount Expected amount of destination token
* @property fromTokenAmount Amount of source token
* @property protocols Selected protocols in a path
* @property estimatedGas gas fee
*/
data class QuoteResponse(
@Json(name = "fromToken") val fromToken: TokenOneInchDto,
@Json(name = "toToken") val toToken: TokenOneInchDto,
@Json(name = "toTokenAmount") val toTokenAmount: String,
@Json(name = "fromTokenAmount") val fromTokenAmount: String,
@Json(name = "protocols") val protocols: List<PathViewDto>,
@Json(name = "estimatedGas") val estimatedGas: Int,
)

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.api.oneinch.models
import com.squareup.moshi.Json
data class StatusResponse(
@Json(name = "status") val status: String,
@Json(name = "provider") val provider: String,
)

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