Updated on 2026-08-14
This commit is contained in:
commit
c83eb5353c
133 changed files with 1851 additions and 807 deletions
|
|
@ -8,6 +8,7 @@ import com.tangem.core.navigation.NavigationAction
|
|||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.wallets.legacy.asLockable
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import kotlinx.coroutines.*
|
||||
import timber.log.Timber
|
||||
import kotlin.time.Duration
|
||||
|
|
@ -125,7 +126,7 @@ internal class LockUserWalletsTimer(
|
|||
if (wasApplicationStopped) {
|
||||
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
|
||||
} else {
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
|
||||
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Welcome))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,14 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.os.bundleOf
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import arrow.core.getOrElse
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
|
|
@ -35,10 +37,13 @@ import com.tangem.data.card.sdk.CardSdkLifecycleObserver
|
|||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
|
||||
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.legacy.asLockable
|
||||
import com.tangem.feature.qrscanning.QrScanningRouter
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
||||
import com.tangem.features.managetokens.navigation.ManageTokensUi
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
|
|
@ -96,6 +101,7 @@ val userWalletsListManagerSafe: UserWalletsListManager? get() = store.state.glob
|
|||
@Deprecated(message = "Provide UserWalletsListManager using DI")
|
||||
val userWalletsListManager: UserWalletsListManager get() = userWalletsListManagerSafe!!
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
|
||||
|
||||
|
|
@ -142,6 +148,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
@Inject
|
||||
lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var getPolkadotCheckHasResetUseCase: GetPolkadotCheckHasResetUseCase
|
||||
|
||||
@Inject
|
||||
lateinit var getPolkadotCheckHasImmortalUseCase: GetPolkadotCheckHasImmortalUseCase
|
||||
|
||||
@Inject
|
||||
lateinit var analyticsEventsHandler: AnalyticsEventHandler
|
||||
|
||||
internal val viewModel: MainViewModel by viewModels()
|
||||
|
||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
|
||||
|
|
@ -172,6 +187,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
|
||||
checkForNotificationPermission()
|
||||
observeStateUpdates()
|
||||
observePolkadotAccountHealthCheck()
|
||||
|
||||
if (intent != null) {
|
||||
deepLinksRegistry.launch(intent)
|
||||
|
|
@ -456,4 +472,25 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observePolkadotAccountHealthCheck() {
|
||||
lifecycleScope.launch {
|
||||
getPolkadotCheckHasResetUseCase()
|
||||
.flowWithLifecycle(lifecycle, minActiveState = Lifecycle.State.CREATED)
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.Token.PolkadotAccountReset(it.second))
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
getPolkadotCheckHasImmortalUseCase()
|
||||
.flowWithLifecycle(lifecycle, minActiveState = Lifecycle.State.CREATED)
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
analyticsEventsHandler.send(
|
||||
WalletScreenAnalyticsEvent.Token.PolkadotImmortalTransactions(it.second),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
|
||||
dialog = when (state.dialog) {
|
||||
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
|
||||
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
|
||||
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(context, state.dialog.source)
|
||||
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
|
||||
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
|
||||
is AppDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import com.tangem.tap.features.demo.DemoHelper
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class CardContextInterceptor(
|
||||
private val scanResponse: ScanResponse?,
|
||||
private val scanResponse: ScanResponse,
|
||||
) : ParamsInterceptor {
|
||||
|
||||
override fun id(): String = CardContextInterceptor.id()
|
||||
|
|
@ -28,8 +28,6 @@ class CardContextInterceptor(
|
|||
}
|
||||
|
||||
override fun intercept(params: MutableMap<String, String>) {
|
||||
scanResponse ?: return
|
||||
|
||||
val card = scanResponse.card
|
||||
params[AnalyticsParam.BATCH] = card.batchId
|
||||
params[AnalyticsParam.PRODUCT_TYPE] = getProductType(scanResponse)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.global
|
|||
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.navigation.StateDialog
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.datasource.config.models.ChatConfig
|
||||
|
|
@ -51,7 +52,11 @@ sealed class GlobalAction : Action {
|
|||
}
|
||||
|
||||
object ScanFailsCounter {
|
||||
data class ChooseBehavior(val result: CompletionResult<ScanResponse>) : GlobalAction()
|
||||
data class ChooseBehavior(
|
||||
val result: CompletionResult<ScanResponse>,
|
||||
val analyticsSource: AnalyticsParam.ScreensSources,
|
||||
) : GlobalAction()
|
||||
|
||||
object Reset : GlobalAction()
|
||||
object Increment : GlobalAction()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.navigation.StateDialog
|
||||
import com.tangem.datasource.config.models.Config
|
||||
|
|
@ -54,14 +55,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
|
|||
when (action.result) {
|
||||
is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset)
|
||||
is CompletionResult.Failure -> {
|
||||
if (action.result.error is TangemSdkError.UserCancelled) {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
|
||||
if (store.state.globalState.scanCardFailsCounter >= 2) {
|
||||
store.dispatchDialogShow(StateDialog.ScanFailsDialog)
|
||||
}
|
||||
} else {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
|
||||
}
|
||||
handleFailureChooseBehaviour(action.result, action.analyticsSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -184,6 +178,26 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleFailureChooseBehaviour(
|
||||
result: CompletionResult.Failure<ScanResponse>,
|
||||
analyticsSource: AnalyticsParam.ScreensSources,
|
||||
) {
|
||||
if (result.error is TangemSdkError.UserCancelled) {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
|
||||
if (store.state.globalState.scanCardFailsCounter >= 2) {
|
||||
val scanFailsSource = when (analyticsSource) {
|
||||
is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN
|
||||
is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS
|
||||
is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO
|
||||
else -> StateDialog.ScanFailsSource.MAIN
|
||||
}
|
||||
store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource))
|
||||
}
|
||||
} else {
|
||||
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreAppCurrency() {
|
||||
scope.launch {
|
||||
val currency = store.inject(DaggerGraphState::appCurrencyRepository)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import android.content.Context
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.navigation.StateDialog
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.feedback.ScanFailsEmail
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -15,12 +17,18 @@ import com.tangem.wallet.R
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object ScanFailsDialog {
|
||||
fun create(context: Context): AlertDialog {
|
||||
fun create(context: Context, source: StateDialog.ScanFailsSource): AlertDialog {
|
||||
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
|
||||
setTitle(context.getString(R.string.common_warning))
|
||||
setMessage(R.string.alert_troubleshooting_scan_card_title)
|
||||
setPositiveButton(R.string.alert_button_request_support) { _, _ ->
|
||||
Analytics.send(Basic.ButtonSupport())
|
||||
val sourceAnalytics = when (source) {
|
||||
StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main
|
||||
StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn
|
||||
StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings
|
||||
StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro
|
||||
}
|
||||
Analytics.send(Basic.ButtonSupport(sourceAnalytics))
|
||||
store.dispatch(GlobalAction.SendEmail(ScanFailsEmail()))
|
||||
}
|
||||
setNeutralButton(R.string.common_cancel) { _, _ -> }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package com.tangem.tap.di
|
|||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
|
||||
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
|
||||
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
|
||||
import com.tangem.tap.domain.sdk.TangemSdkManager
|
||||
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
|
||||
import com.tangem.tap.network.exchangeServices.DefaultRampManager
|
||||
|
|
@ -46,4 +49,20 @@ internal object ActivityModule {
|
|||
fun provideActivityDelayedWorkCoroutineScope(): CoroutineScope {
|
||||
return CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetPolkadotCheckHasResetUseCase(
|
||||
polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
|
||||
): GetPolkadotCheckHasResetUseCase {
|
||||
return GetPolkadotCheckHasResetUseCase(polkadotAccountHealthCheckRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetPolkadotCheckHasImmortalUseCase(
|
||||
polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
|
||||
): GetPolkadotCheckHasImmortalUseCase {
|
||||
return GetPolkadotCheckHasImmortalUseCase(polkadotAccountHealthCheckRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ import dagger.hilt.android.scopes.ViewModelScoped
|
|||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
@Suppress("TooManyFunctions")
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
internal object TokensDomainModule {
|
||||
|
||||
@Provides
|
||||
|
|
@ -341,4 +341,12 @@ internal object TokensDomainModule {
|
|||
): IsAmountSubtractAvailableUseCase {
|
||||
return IsAmountSubtractAvailableUseCase(currenciesRepository, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideRunPolkadotAccountHealthCheckUseCase(
|
||||
repository: PolkadotAccountHealthCheckRepository,
|
||||
): RunPolkadotAccountHealthCheckUseCase {
|
||||
return RunPolkadotAccountHealthCheckUseCase(repository)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.domain.scanCard
|
|||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
|
|
@ -27,7 +27,7 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
|
|||
|
||||
@Suppress("LongParameterList")
|
||||
override suspend fun scan(
|
||||
analyticsEvent: AnalyticsEvent?,
|
||||
analyticsSource: AnalyticsParam.ScreensSources,
|
||||
cardId: String?,
|
||||
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
|
||||
onWalletNotCreated: suspend () -> Unit,
|
||||
|
|
@ -37,7 +37,7 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
|
|||
) {
|
||||
if (isNewCardScanningEnabled) {
|
||||
UseCaseScanProcessor.scan(
|
||||
analyticsEvent,
|
||||
analyticsSource,
|
||||
cardId,
|
||||
onProgressStateChange,
|
||||
onWalletNotCreated,
|
||||
|
|
@ -47,7 +47,7 @@ internal class DefaultScanCardProcessor : ScanCardProcessor {
|
|||
)
|
||||
} else {
|
||||
LegacyScanProcessor.scan(
|
||||
analyticsEvent,
|
||||
analyticsSource,
|
||||
cardId,
|
||||
onProgressStateChange,
|
||||
onWalletNotCreated,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import com.tangem.common.doOnFailure
|
|||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.domain.common.TapWorkarounds.canSkipBackup
|
||||
|
|
@ -45,7 +47,7 @@ internal object LegacyScanProcessor {
|
|||
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun scan(
|
||||
analyticsEvent: AnalyticsEvent?,
|
||||
analyticsSource: AnalyticsParam.ScreensSources,
|
||||
cardId: String?,
|
||||
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
|
||||
onWalletNotCreated: suspend () -> Unit,
|
||||
|
|
@ -59,7 +61,8 @@ internal object LegacyScanProcessor {
|
|||
|
||||
val result = tangemSdkManager.scanProduct(cardId)
|
||||
|
||||
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
|
||||
val analyticsEvent = Basic.CardWasScanned(analyticsSource)
|
||||
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result, analyticsSource))
|
||||
|
||||
result
|
||||
.doOnFailure { error ->
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import arrow.fx.coroutines.resourceScope
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
|
|
@ -44,7 +44,7 @@ internal object UseCaseScanProcessor {
|
|||
|
||||
@Suppress("LongParameterList")
|
||||
suspend fun scan(
|
||||
analyticsEvent: AnalyticsEvent?,
|
||||
analyticsSource: AnalyticsParam.ScreensSources,
|
||||
cardId: String?,
|
||||
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
|
||||
onWalletNotCreated: suspend () -> Unit,
|
||||
|
|
@ -54,10 +54,12 @@ internal object UseCaseScanProcessor {
|
|||
) = progressScope(onProgressStateChange) {
|
||||
val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase)
|
||||
val chains = buildList {
|
||||
add(FailedScansCounterChain(UseCaseScanProcessor::showMaxUnsuccessfulScansReachedDialog))
|
||||
if (analyticsEvent != null) {
|
||||
add(AnalyticsChain(analyticsEvent))
|
||||
}
|
||||
add(
|
||||
FailedScansCounterChain(
|
||||
{ showMaxUnsuccessfulScansReachedDialog(analyticsSource) },
|
||||
),
|
||||
)
|
||||
add(AnalyticsChain(Basic.CardWasScanned(analyticsSource)))
|
||||
add(DisclaimerChain(store, disclaimerWillShow))
|
||||
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager))
|
||||
}
|
||||
|
|
@ -68,8 +70,14 @@ internal object UseCaseScanProcessor {
|
|||
)
|
||||
}
|
||||
|
||||
private fun showMaxUnsuccessfulScansReachedDialog() {
|
||||
store.dispatchDialogShow(StateDialog.ScanFailsDialog)
|
||||
private fun showMaxUnsuccessfulScansReachedDialog(source: AnalyticsParam.ScreensSources) {
|
||||
val scanFailsSource = when (source) {
|
||||
is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN
|
||||
is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS
|
||||
is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO
|
||||
else -> StateDialog.ScanFailsSource.MAIN
|
||||
}
|
||||
store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource))
|
||||
}
|
||||
|
||||
private suspend fun proceedWithException(
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@ internal class BiometricUserWalletsListManager(
|
|||
state.update { State() }
|
||||
}
|
||||
|
||||
override fun isLockable(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
|
||||
if (state.value.selectedUserWalletId == userWalletId) {
|
||||
return@catching findSelectedUserWallet()!!
|
||||
|
|
|
|||
|
|
@ -118,6 +118,10 @@ internal class GeneralUserWalletsListManager(
|
|||
}
|
||||
}
|
||||
|
||||
override fun isLockable(): Boolean {
|
||||
return implementation.value.isLockable()
|
||||
}
|
||||
|
||||
private fun subscribeOnCurrentManager() {
|
||||
appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
|
||||
.distinctUntilChanged()
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
|
|||
state.value.userWallet ?: walletNotFound()
|
||||
}
|
||||
|
||||
override fun isLockable(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
private fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> = catching {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import com.tangem.common.core.TangemError
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.core.UserCodeRequestPolicy
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -493,7 +492,7 @@ class DetailsMiddleware {
|
|||
cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy = shouldSaveAccessCodes)
|
||||
|
||||
store.inject(DaggerGraphState::scanCardProcessor).scan(
|
||||
analyticsEvent = Basic.CardWasScanned(CoreAnalyticsParam.ScannedFrom.MyWallets),
|
||||
analyticsSource = CoreAnalyticsParam.ScreensSources.Settings,
|
||||
onWalletNotCreated = {
|
||||
// No need to rollback policy, continue with the policy set before the card scan
|
||||
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.MutableState
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
|
|
@ -146,7 +147,7 @@ internal class DetailsViewModel(
|
|||
}
|
||||
|
||||
private fun sendFeedback() {
|
||||
Analytics.send(Basic.ButtonSupport())
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings))
|
||||
if (feedbackManagerFeatureToggles.isLocalLogsEnabled) {
|
||||
mainScope.launch {
|
||||
val email = getSupportFeedbackEmailUseCase()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
package com.tangem.tap.features.home.redux
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.rekotlin.Action
|
||||
|
||||
|
|
@ -16,11 +13,9 @@ sealed class HomeAction : Action {
|
|||
/**
|
||||
* Action for scanning card
|
||||
*
|
||||
* @property analyticsEvent analytics event
|
||||
* @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed
|
||||
*/
|
||||
data class ReadCard(
|
||||
val analyticsEvent: AnalyticsEvent? = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Introduction),
|
||||
val scope: CoroutineScope,
|
||||
) : HomeAction()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.tangem.common.doOnResult
|
|||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
|
|
@ -59,7 +59,7 @@ private fun handleHomeAction(action: Action) {
|
|||
}
|
||||
is HomeAction.ReadCard -> {
|
||||
action.scope.launch {
|
||||
readCard(action.analyticsEvent)
|
||||
readCard()
|
||||
}
|
||||
}
|
||||
is HomeAction.GoToShop -> {
|
||||
|
|
@ -75,7 +75,7 @@ private fun handleHomeAction(action: Action) {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun readCard(analyticsEvent: AnalyticsEvent?) {
|
||||
private suspend fun readCard() {
|
||||
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
|
||||
|
||||
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
|
||||
|
|
@ -83,7 +83,7 @@ private suspend fun readCard(analyticsEvent: AnalyticsEvent?) {
|
|||
)
|
||||
|
||||
store.inject(DaggerGraphState::scanCardProcessor).scan(
|
||||
analyticsEvent = analyticsEvent,
|
||||
analyticsSource = AnalyticsParam.ScreensSources.Intro,
|
||||
onProgressStateChange = { showProgress ->
|
||||
if (showProgress) {
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import com.tangem.domain.userwallets.UserWalletIdBuilder
|
|||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -42,7 +43,8 @@ object OnboardingHelper {
|
|||
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
|
||||
val emptyWallets = response.card.wallets.isEmpty()
|
||||
val activationInProgress = onboardingManager?.isActivationInProgress(cardId)
|
||||
val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup
|
||||
val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup &&
|
||||
!DemoHelper.isDemoCard(response)
|
||||
emptyWallets || activationInProgress == true || isNoBackup
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.view.MenuInflater
|
|||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -21,7 +22,7 @@ class OnboardingMenuProvider : MenuProvider {
|
|||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.menu_item_chat_support -> {
|
||||
Analytics.send(Basic.ButtonSupport())
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
|
||||
// changed on email support [REDACTED_TASK_KEY]
|
||||
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
|
||||
true
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.tangem.common.CardIdFormatter
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.CardIdDisplayFormat
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.ui.extensions.setStatusBarColor
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
|
|
@ -502,7 +503,7 @@ class OnboardingWalletFragment :
|
|||
private fun makeSeedPhraseRouter(): SeedPhraseRouter = SeedPhraseRouter(
|
||||
onBack = ::legacyOnBackHandler,
|
||||
onOpenChat = {
|
||||
Analytics.send(Basic.ButtonSupport())
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
|
||||
// changed on email support [REDACTED_TASK_KEY]
|
||||
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.app.Dialog
|
|||
import android.content.Context
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
|
|
@ -21,7 +22,7 @@ object WalletActivationErrorDialog {
|
|||
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() }
|
||||
setNegativeButton(R.string.common_support) { _, _ ->
|
||||
// changed on email support [REDACTED_TASK_KEY]
|
||||
Analytics.send(Basic.ButtonSupport())
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
|
||||
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
|
||||
}
|
||||
setOnDismissListener { store.dispatchDialogHide() }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.features.send.ui.dialogs
|
|||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -21,7 +22,7 @@ object RequestFeeErrorDialog {
|
|||
setTitle(R.string.common_fee_error)
|
||||
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
|
||||
setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ ->
|
||||
Analytics.send(Basic.ButtonSupport())
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
|
||||
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
|
||||
}
|
||||
setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.common.module.ModuleMessageConverter
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.sdk.extensions.localizedDescription
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
|
|
@ -33,7 +34,7 @@ object SendTransactionFailsDialog {
|
|||
setTitle(R.string.alert_failed_to_send_transaction_title)
|
||||
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
|
||||
setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ ->
|
||||
Analytics.send(Basic.ButtonSupport())
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
|
||||
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
|
||||
}
|
||||
setPositiveButton(R.string.common_cancel) { _, _ -> }
|
||||
|
|
|
|||
|
|
@ -4,20 +4,20 @@ import androidx.compose.animation.AnimatedVisibility
|
|||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.key
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.TextUnitType
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState
|
||||
import kotlinx.collections.immutable.ImmutableCollection
|
||||
|
|
@ -41,10 +41,7 @@ internal fun BriefNetworksList(
|
|||
exit = fadeOut(),
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
|
||||
val iterator = networks.iterator()
|
||||
var index = 0
|
||||
while (iterator.hasNext()) {
|
||||
val network = iterator.next()
|
||||
for ((index, network) in networks.withIndex()) {
|
||||
if (index < MAX_VISIBLE_BRIEF_ICONS) {
|
||||
key(network.name + network.protocolName) {
|
||||
BriefNetworkItem(model = network)
|
||||
|
|
@ -59,7 +56,6 @@ internal fun BriefNetworksList(
|
|||
break
|
||||
}
|
||||
}
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -109,8 +105,16 @@ internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modi
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
internal fun HasMoreItem(moreCount: Int) {
|
||||
val count = if (moreCount > 99) 99 else moreCount
|
||||
val themeTextStyle = TangemTheme.typography.overline.copy(
|
||||
letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp),
|
||||
)
|
||||
var textStyle by remember(themeTextStyle) { mutableStateOf(themeTextStyle) }
|
||||
var readyToDraw by remember(themeTextStyle) { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size20)
|
||||
|
|
@ -118,10 +122,20 @@ internal fun HasMoreItem(moreCount: Int) {
|
|||
.background(TangemTheme.colors.control.unchecked),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = "+$moreCount",
|
||||
style = TangemTheme.typography.overline,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing4)
|
||||
.align(Alignment.Center)
|
||||
.drawWithContent { if (readyToDraw) drawContent() },
|
||||
text = "+$count",
|
||||
style = textStyle,
|
||||
overflow = TextOverflow.Clip,
|
||||
onTextLayout = { textLayoutResult ->
|
||||
if (textLayoutResult.didOverflowHeight) {
|
||||
textStyle = textStyle.copy(fontSize = textStyle.fontSize * 0.9)
|
||||
} else {
|
||||
readyToDraw = true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,10 +18,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockTy
|
|||
import com.tangem.domain.wallets.legacy.unlockIfLockable
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
|
||||
|
|
@ -141,7 +138,7 @@ internal class WelcomeMiddleware {
|
|||
val currency = ParamCardCurrencyConverter().convert(
|
||||
value = scanResponse.cardTypesResolver,
|
||||
)
|
||||
|
||||
Analytics.addContext(scanResponse)
|
||||
if (currency != null) {
|
||||
Analytics.send(
|
||||
event = Basic.SignedIn(
|
||||
|
|
@ -175,7 +172,7 @@ internal class WelcomeMiddleware {
|
|||
)
|
||||
|
||||
store.inject(DaggerGraphState::scanCardProcessor).scan(
|
||||
analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.SignIn),
|
||||
analyticsSource = AnalyticsParam.ScreensSources.SignIn,
|
||||
onSuccess = { scanResponse ->
|
||||
scope.launch { onCardScanned(scanResponse) }
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
|
|||
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder
|
||||
import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager
|
||||
import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarMemo
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
|
||||
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
|
||||
|
|
@ -170,7 +170,7 @@ class TransactionManagerImpl(
|
|||
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
|
||||
val walletManager = getActualWalletManager(blockchain, derivationPath)
|
||||
if (walletManager is EthereumWalletManager) {
|
||||
if (walletManager is OptimismWalletManager) {
|
||||
if (walletManager is EthereumOptimisticRollupWalletManager) {
|
||||
return getFeeForOptimismBlockchain(
|
||||
walletManager = walletManager,
|
||||
amount = createAmount(amountToSend, currencyToSend, blockchain),
|
||||
|
|
@ -285,7 +285,7 @@ class TransactionManagerImpl(
|
|||
}
|
||||
|
||||
private suspend fun getFeeForOptimismBlockchain(
|
||||
walletManager: OptimismWalletManager,
|
||||
walletManager: EthereumOptimisticRollupWalletManager,
|
||||
amount: Amount,
|
||||
destinationAddress: String,
|
||||
data: String?,
|
||||
|
|
|
|||
|
|
@ -57,11 +57,13 @@ sealed class AnalyticsParam {
|
|||
object BlockchainSdk : Error("Blockchain Sdk Error")
|
||||
}
|
||||
|
||||
sealed class ScannedFrom(val value: String) {
|
||||
object Introduction : ScannedFrom("Introduction")
|
||||
object Main : ScannedFrom("Main")
|
||||
object SignIn : ScannedFrom("Sign In")
|
||||
object MyWallets : ScannedFrom("My Wallets")
|
||||
sealed class ScreensSources(val value: String) {
|
||||
data object Settings : ScreensSources("Settings")
|
||||
data object Main : ScreensSources("Main")
|
||||
data object SignIn : ScreensSources("Sign In")
|
||||
data object Send : ScreensSources("Send")
|
||||
data object Intro : ScreensSources("Introduction")
|
||||
data object MyWallets : ScreensSources("My Wallets")
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sealed class Basic(
|
|||
) : AnalyticsEvent("Basic", event, params, error) {
|
||||
|
||||
class CardWasScanned(
|
||||
source: AnalyticsParam.ScannedFrom,
|
||||
source: AnalyticsParam.ScreensSources,
|
||||
) : Basic(
|
||||
event = "Card Was Scanned",
|
||||
params = mapOf(
|
||||
|
|
@ -76,5 +76,10 @@ sealed class Basic(
|
|||
|
||||
class WalletOpened : Basic(event = "Wallet Opened")
|
||||
|
||||
class ButtonSupport : Basic("Request Support")
|
||||
class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic(
|
||||
event = "Request Support",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -146,8 +146,9 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
|
|||
blockBookRest = accessTokens.bitcoin?.blockBookRest,
|
||||
),
|
||||
algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest),
|
||||
zkSyncEra = GetBlockAccessToken(jsonRpc = accessTokens.zksync?.jsonRPC),
|
||||
polygonZkEvm = GetBlockAccessToken(jsonRpc = accessTokens.polygonZkevm?.jsonRPC),
|
||||
zkSyncEra = GetBlockAccessToken(rest = accessTokens.zksync?.jsonRPC),
|
||||
polygonZkEvm = GetBlockAccessToken(rest = accessTokens.polygonZkevm?.jsonRPC),
|
||||
base = GetBlockAccessToken(rest = accessTokens.base?.jsonRPC),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ data class GetBlockAccessTokens(
|
|||
@Json(name = "algorand") val algorand: GetBlockToken?,
|
||||
@Json(name = "polygon-zkevm") val polygonZkevm: GetBlockToken?,
|
||||
@Json(name = "zksync") val zksync: GetBlockToken?,
|
||||
@Json(name = "base") val base: GetBlockToken?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ class AppPreferencesStore(
|
|||
return this[key]?.let(adapter::fromJson).orEmpty()
|
||||
}
|
||||
|
||||
/** Get set of data [T] by string [key] */
|
||||
inline fun <reified T> MutablePreferences.getObjectSet(key: Preferences.Key<String>): Set<T>? {
|
||||
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
|
||||
return this[key]?.let(adapter::fromJson)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data [T] by string [key] to [MutablePreferences]
|
||||
*
|
||||
|
|
@ -97,4 +103,10 @@ class AppPreferencesStore(
|
|||
|
||||
this[key] = adapter.toJson(value)
|
||||
}
|
||||
|
||||
/** Sets set of data [T] by string [key] to [MutablePreferences] */
|
||||
inline fun <reified T> MutablePreferences.setObjectSet(key: Preferences.Key<String>, value: Set<T>) {
|
||||
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
|
||||
this[key] = adapter.toJson(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,16 @@ object PreferencesKeys {
|
|||
|
||||
val APP_LOGS_KEY by lazy { stringPreferencesKey(name = "app_logs") }
|
||||
|
||||
val POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY by lazy {
|
||||
stringPreferencesKey(name = "POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX")
|
||||
}
|
||||
val POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY by lazy {
|
||||
stringPreferencesKey(name = "POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS")
|
||||
}
|
||||
val POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY by lazy {
|
||||
stringPreferencesKey(name = "POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS")
|
||||
}
|
||||
|
||||
val SEND_TAP_HELP_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "sendTapHelpPreview") }
|
||||
|
||||
val WAS_APPLICATION_STOPPED_KEY by lazy { booleanPreferencesKey(name = "applicationStopped") }
|
||||
|
|
|
|||
|
|
@ -128,6 +128,15 @@ suspend inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences
|
|||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
/** Get set of data [T] by string [key], or empty if data is not found */
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Preferences.Key<String>): Set<T> {
|
||||
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
},
|
||||
{
|
||||
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.9.0"
|
||||
},
|
||||
{
|
||||
"name": "LOCAL_USER_LOGS_ENABLED",
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
},
|
||||
{
|
||||
"name": "WC_SOLANA_TX_SIGN_ENABLED",
|
||||
"version": "5.9.0"
|
||||
"version": "5.11.0"
|
||||
},
|
||||
{
|
||||
"name": "TOKEN_LIST_LCE_ENABLED",
|
||||
|
|
|
|||
|
|
@ -2,5 +2,9 @@ package com.tangem.core.navigation
|
|||
|
||||
interface StateDialog {
|
||||
|
||||
object ScanFailsDialog : StateDialog
|
||||
data class ScanFailsDialog(val source: ScanFailsSource) : StateDialog
|
||||
|
||||
enum class ScanFailsSource {
|
||||
MAIN, SIGN_IN, SETTINGS, INTRO;
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@
|
|||
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
|
||||
<string name="common_explorer">Обозреватель</string>
|
||||
<string name="common_fee_label">Комиссия</string>
|
||||
<string name="common_fee_selector_footer">Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s</string>
|
||||
<string name="common_fee_selector_footer">Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s</string>
|
||||
<string name="common_fee_selector_option_custom">Свое</string>
|
||||
<string name="common_fee_selector_option_fast">Быстро</string>
|
||||
<string name="common_fee_selector_option_market">По рынку</string>
|
||||
|
|
@ -310,7 +310,7 @@
|
|||
<string name="onboarding_exit_alert_message">В этом случае вам будет необходимо начать процесс заново.</string>
|
||||
<string name="onboarding_exit_alert_title">Вы хотите выйти из процесса активации?</string>
|
||||
<string name="onboarding_getting_started">Подготовка</string>
|
||||
<string name="onboarding_linking_error_card_with_wallets">Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа?</string>
|
||||
<string name="onboarding_linking_error_card_with_wallets">Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Если на нем есть средства, пожалуйста сначала выведите их, а затем сделайте сброс до заводских настроек и используйте как резервную.</string>
|
||||
<string name="onboarding_navbar_title_creating_backup">Резервная копия</string>
|
||||
<string name="onboarding_seed_button_read_more">Прочитать о seed-фразе</string>
|
||||
<plurals name="onboarding_seed_generate_message_words_count">
|
||||
|
|
@ -426,12 +426,12 @@
|
|||
<string name="scan_card_settings_title">Приготовьте свою карту</string>
|
||||
<string name="send_additional_field_already_included">Уже содержится в введенном адресе</string>
|
||||
<string name="send_alert_fee_coverage_subract_text">Вычесть</string>
|
||||
<string name="send_alert_fee_coverage_title">Недостаточно средств для покрытия комиссии сети. Вычесть комиссию %s из отправляемой сумму?</string>
|
||||
<string name="send_alert_fee_coverage_title">Недостаточно средств для покрытия комиссии сети. Вычесть недостающую сумму для покрытия комиссии из отправляемой суммы?</string>
|
||||
<string name="send_alert_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
|
||||
<string name="send_alert_fee_too_low_text">Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить?</string>
|
||||
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
|
||||
<string name="send_alert_transaction_failed_title">Транзакция не выполнена</string>
|
||||
<string name="send_amount_label">Сумма</string>
|
||||
<string name="send_confirm_label">Подтверждение</string>
|
||||
<string name="send_date_format">%1$s, %2$s</string>
|
||||
<string name="send_destination_hint_address">Адрес</string>
|
||||
<string name="send_destination_tag_field">Код назначения</string>
|
||||
|
|
@ -487,6 +487,7 @@
|
|||
<string name="send_recipient_label">Отправить</string>
|
||||
<string name="send_recipient_memo_footer">Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.</string>
|
||||
<string name="send_recipient_wallets_title">Мои кошельки</string>
|
||||
<string name="send_satoshi_per_byte_text">Это способ измерения комиссии за отправку биткоин-транзакции. Он указывает на количество самой маленькой единицы биткоина (сатоши) за каждый байт данных в транзакции. Чем выше это число, тем быстрее будет обработана транзакция сетью.</string>
|
||||
<string name="send_sending">Отправка</string>
|
||||
<string name="send_summary_tap_hint">Нажмите на любое поле, чтобы изменить его</string>
|
||||
<string name="send_summary_title">Отправка %s</string>
|
||||
|
|
@ -536,7 +537,8 @@
|
|||
<string name="toast_balances_hidden">Балансы скрыты</string>
|
||||
<string name="toast_balances_shown">Балансы показаны</string>
|
||||
<string name="toast_undo">Отменить</string>
|
||||
<string name="token_button_unavailability_reason_buy_unavailable">В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением.”</string>
|
||||
<string name="token_button_unavailability_generic_description">Выбранная операция в данный момент недоступна. Попробуйте позже.</string>
|
||||
<string name="token_button_unavailability_reason_buy_unavailable">В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением.</string>
|
||||
<string name="token_button_unavailability_reason_empty_balance">У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства.</string>
|
||||
<string name="token_button_unavailability_reason_no_quotes">Выбранная операция в данный момент недоступна. Попробуйте позже.</string>
|
||||
<string name="token_button_unavailability_reason_not_exchangeable">Обмен %s не доступен. Но мы работаем над его добавлением.</string>
|
||||
|
|
@ -547,6 +549,7 @@
|
|||
<string name="token_details_hide_alert_title">Скрыть %s</string>
|
||||
<string name="token_details_hide_token">Скрыть токен</string>
|
||||
<string name="token_details_token_type_subtitle">%1$s токен в сети %%image%% %2$s</string>
|
||||
<string name="token_details_token_type_subtitle_no_standard">Токен в сети %%image%% %1$s</string>
|
||||
<string name="token_details_unable_hide_alert_message">Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.</string>
|
||||
<string name="token_details_unable_hide_alert_title">Невозможно скрыть %s</string>
|
||||
<string name="token_swap_changelly_promotion_message">Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля.</string>
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@
|
|||
<string name="onboarding_exit_alert_message">In this case, you will need to start from the beginning.</string>
|
||||
<string name="onboarding_exit_alert_title">Do you want to exit the activation process?</string>
|
||||
<string name="onboarding_getting_started">Getting started</string>
|
||||
<string name="onboarding_linking_error_card_with_wallets">Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet?</string>
|
||||
<string name="onboarding_linking_error_card_with_wallets">Another wallet has already been created on the card you\'re trying to add. If you have funds in this wallet, please withdraw it and then reset this card and add it as a backup.</string>
|
||||
<string name="onboarding_navbar_title_creating_backup">Creating a backup</string>
|
||||
<string name="onboarding_seed_button_read_more">Read more about seed phrase</string>
|
||||
<plurals name="onboarding_seed_generate_message_words_count">
|
||||
|
|
@ -421,12 +421,12 @@
|
|||
<string name="scan_card_settings_title">Get your card ready!</string>
|
||||
<string name="send_additional_field_already_included">Already included in the entered address</string>
|
||||
<string name="send_alert_fee_coverage_subract_text">Subtract</string>
|
||||
<string name="send_alert_fee_coverage_title">Not enough funds to cover the network commission. Subtract the commission %s from the amount sent?</string>
|
||||
<string name="send_alert_fee_coverage_title">Not enough funds to cover the network fee. Do you want to subtract the amount required to cover the fee?</string>
|
||||
<string name="send_alert_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
|
||||
<string name="send_alert_fee_too_low_text">You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue?</string>
|
||||
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
|
||||
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
|
||||
<string name="send_amount_label">Amount</string>
|
||||
<string name="send_confirm_label">Confirm</string>
|
||||
<string name="send_date_format">%1$s, %2$s</string>
|
||||
<string name="send_destination_hint_address">Address</string>
|
||||
<string name="send_destination_tag_field">Destination Tag</string>
|
||||
|
|
@ -482,6 +482,8 @@
|
|||
<string name="send_recipient_label">Send to</string>
|
||||
<string name="send_recipient_memo_footer">A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds</string>
|
||||
<string name="send_recipient_wallets_title">My wallets</string>
|
||||
<string name="send_satoshi_per_byte_text">The fee for a Bitcoin transaction is measured by the number of the smallest Bitcoin unit (Satoshi) per byte of data. The higher this number, the faster the transaction will be processed.</string>
|
||||
<string name="send_satoshi_per_byte_title">Satoshi per vbyte</string>
|
||||
<string name="send_sending">Sending...</string>
|
||||
<string name="send_summary_tap_hint">Tap any field to change it</string>
|
||||
<string name="send_summary_title">Send %s</string>
|
||||
|
|
@ -532,6 +534,7 @@
|
|||
<string name="toast_balances_hidden">Balances hidden</string>
|
||||
<string name="toast_balances_shown">Balances shown</string>
|
||||
<string name="toast_undo">Undo</string>
|
||||
<string name="token_button_unavailability_generic_description">This operation is currently unavailable. Please try again later.</string>
|
||||
<string name="token_button_unavailability_reason_buy_unavailable">The purchase of the %s is currently unavailable. But we are working on adding it.</string>
|
||||
<string name="token_button_unavailability_reason_empty_balance">You do not have funds to send. Top up your account to be able to send funds from it.</string>
|
||||
<string name="token_button_unavailability_reason_no_quotes">This operation is currently unavailable. Please try again later.</string>
|
||||
|
|
@ -543,6 +546,7 @@
|
|||
<string name="token_details_hide_alert_title">Hide %s</string>
|
||||
<string name="token_details_hide_token">Hide token</string>
|
||||
<string name="token_details_token_type_subtitle">%1$s token in %%image%% %2$s network</string>
|
||||
<string name="token_details_token_type_subtitle_no_standard">Token in %%image%% %1$s network</string>
|
||||
<string name="token_details_unable_hide_alert_message">The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list.</string>
|
||||
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
|
||||
<string name="token_swap_changelly_promotion_message">Exchange this token for another at %1$s service fees from February %2$s-%3$s.</string>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,16 @@ fun SimpleTextField(
|
|||
}
|
||||
}
|
||||
|
||||
var lastTextValue by remember(value) { mutableStateOf(value) }
|
||||
var lastTextValue by remember(value) {
|
||||
val isSelectionLastIndex = textFieldValueState.selection.end == textFieldValueState.text.lastIndex
|
||||
if (textFieldValueState.text.isBlank() || isSelectionLastIndex) {
|
||||
textFieldValueState = textFieldValueState.copy(
|
||||
text = value,
|
||||
selection = getValueRange(value),
|
||||
)
|
||||
}
|
||||
mutableStateOf(value)
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides customTextSelectionColors) {
|
||||
BasicTextField(
|
||||
|
|
@ -82,9 +91,7 @@ fun SimpleTextField(
|
|||
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
|
||||
lastTextValue = newTextFieldValueState.text
|
||||
|
||||
if (stringChangedSinceLastInvocation) {
|
||||
onValueChange(newTextFieldValueState.text)
|
||||
}
|
||||
if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text)
|
||||
},
|
||||
textStyle = textStyle.copy(color = color),
|
||||
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import androidx.compose.ui.text.input.VisualTransformation
|
|||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.defaultFormat
|
||||
import com.tangem.core.ui.utils.formatWithThousands
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class AmountVisualTransformation(
|
||||
|
|
@ -23,21 +22,16 @@ class AmountVisualTransformation(
|
|||
decimals,
|
||||
)
|
||||
formattedAmount = formattedAmount.ifEmpty { decimalFormat.defaultFormat() }
|
||||
val decimalValue = text.text.parseToBigDecimal(decimals)
|
||||
val formattedText = if (formattedAmount.isNotEmpty() && symbol != null) {
|
||||
AnnotatedString(
|
||||
if (currencyCode != null) {
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = decimalValue,
|
||||
BigDecimalFormatter.formatFiatEditableAmount(
|
||||
fiatAmount = formattedAmount,
|
||||
fiatCurrencyCode = currencyCode,
|
||||
fiatCurrencySymbol = symbol,
|
||||
)
|
||||
} else {
|
||||
BigDecimalFormatter.formatCryptoAmountUncapped(
|
||||
cryptoAmount = decimalValue,
|
||||
cryptoSymbol = symbol,
|
||||
decimals = decimals,
|
||||
)
|
||||
BigDecimalFormatter.formatWithSymbol(formattedAmount, symbol)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ fun InputRowEnterInfoAmount(
|
|||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
showDivider: Boolean = false,
|
||||
isReadOnly: Boolean = false,
|
||||
) {
|
||||
DividerContainer(
|
||||
modifier = modifier,
|
||||
|
|
@ -83,6 +84,7 @@ fun InputRowEnterInfoAmount(
|
|||
),
|
||||
onValueChange = onValueChange,
|
||||
color = textColor,
|
||||
isEnabled = !isReadOnly,
|
||||
textStyle = TangemTheme.typography.body2,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
|
@ -38,30 +40,6 @@ object BigDecimalFormatter {
|
|||
}
|
||||
}
|
||||
|
||||
fun formatCryptoAmountUncapped(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoSymbol: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatter = NumberFormat.getNumberInstance(locale).apply {
|
||||
maximumFractionDigits = decimals
|
||||
minimumFractionDigits = minOf(2, decimals)
|
||||
isGroupingUsed = true
|
||||
roundingMode = RoundingMode.DOWN
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount).let {
|
||||
if (cryptoSymbol.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it + "\u2009$cryptoSymbol"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal?,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
|
|
@ -90,6 +68,26 @@ object BigDecimalFormatter {
|
|||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
fun formatFiatEditableAmount(
|
||||
fiatAmount: String?,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
if (fiatAmount == null) return EMPTY_BALANCE_SIGN
|
||||
|
||||
val formatterCurrency = getCurrency(fiatCurrencyCode)
|
||||
val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
}
|
||||
val formatter = requireNotNull(numberFormatter as? DecimalFormat) {
|
||||
Timber.e("NumberFormat is null")
|
||||
return EMPTY_BALANCE_SIGN
|
||||
}
|
||||
return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}"
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
fun formatPercent(
|
||||
percent: BigDecimal,
|
||||
useAbsoluteValue: Boolean,
|
||||
|
|
@ -107,6 +105,8 @@ object BigDecimalFormatter {
|
|||
return formatter.format(value)
|
||||
}
|
||||
|
||||
fun formatWithSymbol(amount: String, symbol: String) = "$amount\u2009$symbol"
|
||||
|
||||
private fun getCurrency(code: String): Currency {
|
||||
return runCatching { Currency.getInstance(code) }
|
||||
.getOrElse { e ->
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import java.util.Locale
|
|||
|
||||
private const val TEXT_CHUNK_THOUSAND = 3
|
||||
private const val POINT_SEPARATOR = '.'
|
||||
private const val COMMA_SEPARATOR = ','
|
||||
private const val SCIENTIFIC_NOTATION = 'e'
|
||||
const val DECIMAL_SEPARATOR_LIMIT = 1
|
||||
|
||||
@Composable
|
||||
fun rememberDecimalFormat(): DecimalFormat {
|
||||
|
|
@ -59,7 +62,7 @@ fun DecimalFormat.getValidatedNumberWithFixedDecimals(text: String, decimals: In
|
|||
return if (filteredChars.count { it == decimalSeparator } == 1) {
|
||||
val beforeDecimal = filteredChars.substringBefore(decimalSeparator)
|
||||
val afterDecimal = filteredChars.substringAfter(decimalSeparator)
|
||||
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
|
||||
decimals.getWithIntegerDecimals(beforeDecimal, decimalSeparator, afterDecimal)
|
||||
}
|
||||
// If there is no dot, just take all digits
|
||||
else {
|
||||
|
|
@ -82,7 +85,7 @@ fun DecimalFormat.formatWithThousands(text: String, decimals: Int): String {
|
|||
.joinToString(thousandsSeparator.toString())
|
||||
.reversed()
|
||||
val afterDecimal = localizedText.substringAfter(decimalSeparator)
|
||||
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
|
||||
decimals.getWithIntegerDecimals(beforeDecimal, decimalSeparator, afterDecimal)
|
||||
}
|
||||
// If there is no dot, just take all digits
|
||||
else {
|
||||
|
|
@ -150,4 +153,51 @@ fun BigDecimal.parseBigDecimal(decimals: Int, roundingMode: RoundingMode = Round
|
|||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal amount string parser to [BigDecimal]
|
||||
* Able to parse values with only ONE separator, assuming separator is COMMA.
|
||||
* Otherwise returns null.
|
||||
*/
|
||||
fun String.parseBigDecimalOrNull() = runCatching {
|
||||
// Filtering value containing more than one either grouping or decimal separator.
|
||||
// We assume there will be only decimal separator, otherwise parsing will fail.
|
||||
|
||||
// Step 1. Exclude formatted (100,000.0) except scientific notation (100.000e10)
|
||||
val excludeFormatted = this.count {
|
||||
!it.isDigit() && !it.equals(SCIENTIFIC_NOTATION, ignoreCase = true)
|
||||
} > DECIMAL_SEPARATOR_LIMIT
|
||||
|
||||
// Step 2. Exclude wrong scientific notation (100e100e100)
|
||||
val excludeWrongScientific = this.count {
|
||||
it.equals(SCIENTIFIC_NOTATION, ignoreCase = true)
|
||||
} > DECIMAL_SEPARATOR_LIMIT
|
||||
if (excludeFormatted || excludeWrongScientific) return null
|
||||
|
||||
// An attempt to parse value with POINT decimal separator
|
||||
val parsed = this.toBigDecimalOrNull()
|
||||
|
||||
if (parsed == null) {
|
||||
// If parsing with POINT separator fails trying to parse with COMMA separator
|
||||
val decimalFormatSymbol = DecimalFormatSymbols().apply {
|
||||
decimalSeparator = COMMA_SEPARATOR
|
||||
}
|
||||
val decimalFormat = DecimalFormat().apply {
|
||||
decimalFormatSymbols = decimalFormatSymbol
|
||||
isParseBigDecimal = true
|
||||
}
|
||||
|
||||
// Return either number or null if fails
|
||||
decimalFormat.parse(this) as? BigDecimal
|
||||
} else {
|
||||
// If parsing with POINT separator succeeds return number
|
||||
parsed
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
private fun Int.getWithIntegerDecimals(before: String, separator: Char, after: String): String = if (this == 0) {
|
||||
before
|
||||
} else {
|
||||
before + separator + after.take(this)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.data.qrscanning.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.utils.parseBigDecimalOrNull
|
||||
import com.tangem.domain.qrscanning.models.QrResult
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
|
||||
|
|
@ -44,7 +45,7 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
|
|||
when (it.key) {
|
||||
Parameter.Amount -> {
|
||||
// According to BIP-0021, the value is specified in decimals. No conversion needed
|
||||
result.amount = it.value.toBigDecimalOrNull()
|
||||
result.amount = it.value.parseBigDecimalOrNull()
|
||||
}
|
||||
Parameter.Message,
|
||||
Parameter.Memo,
|
||||
|
|
@ -52,16 +53,17 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
|
|||
result.memo = URLDecoder.decode(it.value, "UTF-8")
|
||||
}
|
||||
Parameter.Address -> {
|
||||
// If 'address' parameter is exists, then currency must be TOKEN.
|
||||
val tokenCurrency = cryptoCurrency as? CryptoCurrency.Token ?: return QrResult()
|
||||
|
||||
// Overrides destination address for token transfers (ERC-681)
|
||||
if (cryptoCurrency is CryptoCurrency.Token) {
|
||||
// `address` parameter is used only if the contract address, encoded in the QR,
|
||||
// matches the contract address of the token.
|
||||
// Otherwise, the scanned string is likely malformed, and we stop the entire parsing routine
|
||||
if (cryptoCurrency.contractAddress.equals(address, ignoreCase = true)) {
|
||||
result.address = it.value
|
||||
} else {
|
||||
return QrResult()
|
||||
}
|
||||
// `address` parameter is used only if the contract address, encoded in the QR,
|
||||
// matches the contract address of the token.
|
||||
// Otherwise, the scanned string is likely malformed, and we stop the entire parsing routin
|
||||
if (tokenCurrency.contractAddress.equals(address, ignoreCase = true)) {
|
||||
result.address = it.value
|
||||
} else {
|
||||
return QrResult()
|
||||
}
|
||||
}
|
||||
Parameter.Value,
|
||||
|
|
@ -69,7 +71,7 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
|
|||
-> {
|
||||
// Extra convert parses scientific notation to decimal
|
||||
// This is necessary to be able comparing BigDecimal values
|
||||
result.amount = it.value.toBigDecimalOrNull()
|
||||
result.amount = it.value.parseBigDecimalOrNull()
|
||||
?.toPlainString()?.toBigDecimalOrNull()
|
||||
?.divide(BigDecimal.TEN.pow(cryptoCurrency.decimals))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,11 +152,6 @@ internal class DefaultQrScanningEventsRepositoryTest {
|
|||
QrResult(address = address2),
|
||||
cryptoCurrency,
|
||||
)
|
||||
positiveCase(
|
||||
"$garbage$schema2:$address2$function?$addressParam=$addressParamValue",
|
||||
QrResult(address = address2),
|
||||
cryptoCurrency,
|
||||
)
|
||||
positiveCase(
|
||||
"$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$someAmountParamValue",
|
||||
QrResult(address = address2),
|
||||
|
|
@ -177,6 +172,11 @@ internal class DefaultQrScanningEventsRepositoryTest {
|
|||
QrResult(address = address2, amount = BigDecimal("0.000000023")),
|
||||
cryptoCurrency,
|
||||
)
|
||||
negativeCase(
|
||||
"$garbage$schema2:$address2$function?$addressParam=$addressParamValue",
|
||||
QrResult(address = address2),
|
||||
cryptoCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -232,11 +232,21 @@ internal class DefaultQrScanningEventsRepositoryTest {
|
|||
QrResult(address = address2, amount = BigDecimal("2300")),
|
||||
tokenCryptoCurrency,
|
||||
)
|
||||
positiveCase(
|
||||
"$address4?$addressParam=$addressParamValue",
|
||||
QrResult(address = addressParamValue),
|
||||
tokenCryptoCurrency,
|
||||
)
|
||||
negativeCase(
|
||||
"$address2?$someParam=$someParamValue&$amountParam=$amountParamValue",
|
||||
QrResult(address = address2, amount = BigDecimal("123.123"), memo = memoParamValueUtf8),
|
||||
tokenCryptoCurrency,
|
||||
)
|
||||
negativeCase(
|
||||
"$address4?$addressParam=$addressParamValue",
|
||||
QrResult(address = addressParamValue),
|
||||
cryptoCurrency,
|
||||
)
|
||||
}
|
||||
|
||||
private fun positiveCase(input: String, expected: QrResult, cryptoCurrency: CryptoCurrency) {
|
||||
|
|
|
|||
|
|
@ -119,4 +119,18 @@ internal object TokensDataModule {
|
|||
fun provideCurrencyChecksRepository(walletManagersFacade: WalletManagersFacade): CurrencyChecksRepository {
|
||||
return DefaultCurrencyChecksRepository(walletManagersFacade = walletManagersFacade)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePolkadotAccountHealthCheckRepository(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): PolkadotAccountHealthCheckRepository {
|
||||
return DefaultPolkadotAccountHealthCheckRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ import kotlinx.coroutines.*
|
|||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultNetworksRepository(
|
||||
private val networksStatusesStore: NetworksStatusesStore,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
package com.tangem.data.tokens.repository
|
||||
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.tangem.blockchain.blockchains.polkadot.AccountCheckProvider
|
||||
import com.tangem.blockchain.blockchains.polkadot.network.accounthealthcheck.ExtrinsicListItemResponse
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMap
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSetSync
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.collections.set
|
||||
|
||||
internal class DefaultPolkadotAccountHealthCheckRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : PolkadotAccountHealthCheckRepository {
|
||||
|
||||
private val hasImmortalTransaction = MutableSharedFlow<Pair<String, Boolean>>()
|
||||
private val hasResetTransaction = MutableSharedFlow<Pair<String, Boolean>>()
|
||||
|
||||
private val mutex = Mutex()
|
||||
private val mutexes = ConcurrentHashMap<String, Mutex>()
|
||||
|
||||
override suspend fun runCheck(userWalletId: UserWalletId, network: Network) {
|
||||
// Run Polkadot account health check
|
||||
if (Blockchain.fromId(network.id.value) != Blockchain.Polkadot) return
|
||||
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network)
|
||||
val address = requireNotNull(walletManager?.wallet?.address) {
|
||||
Timber.e("Address is null")
|
||||
return
|
||||
}
|
||||
val accountCheckProvider = requireNotNull(walletManager as? AccountCheckProvider) {
|
||||
Timber.e("Unable to cast wallet manager to AccountCheckProvider")
|
||||
return
|
||||
}
|
||||
|
||||
// use a separate mutexForKey for each key to avoid multiple calls block() to the same key
|
||||
// also used mutex to safe create mutexForKey, otherwise it can lead to multiple calls for the same key
|
||||
val mutexForKey = mutex.withLock { mutexes.getOrPut(address) { Mutex() } }
|
||||
mutexForKey.withLock {
|
||||
withContext(dispatchers.io) {
|
||||
checkHasReset(accountCheckProvider, address)
|
||||
checkHasImmortal(accountCheckProvider, address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun subscribeToHasImmortalResults() = hasImmortalTransaction.asSharedFlow()
|
||||
|
||||
override fun subscribeToHasResetResults() = hasResetTransaction.asSharedFlow()
|
||||
|
||||
private suspend fun checkHasReset(polkadotManager: AccountCheckProvider, address: String) {
|
||||
val checkedAddresses = appPreferencesStore.getObjectSetSync<String>(POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY)
|
||||
if (checkedAddresses.contains(address)) return
|
||||
runCatching {
|
||||
val accountInfo = requireNotNull(polkadotManager.getAccountInfo().account) {
|
||||
Timber.e("Account info is null")
|
||||
}
|
||||
val nonce = accountInfo.nonce
|
||||
val extrinsicCount = accountInfo.countExtrinsic
|
||||
|
||||
// Account was reset
|
||||
if (nonce != null && extrinsicCount != null) {
|
||||
val hasReset = nonce < extrinsicCount
|
||||
updateChecked(address, POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY)
|
||||
hasResetTransaction.emit(address to hasReset)
|
||||
}
|
||||
}.onFailure {
|
||||
if ((it as? BlockchainSdkError.CustomError)?.customMessage == ACCOUNT_NOT_FOUND) {
|
||||
updateChecked(address, POLKADOT_HEALTH_CHECKED_RESET_ACCOUNTS_KEY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkHasImmortal(accountCheckerProvider: AccountCheckProvider, address: String) {
|
||||
val checkedAddresses =
|
||||
appPreferencesStore.getObjectSetSync<String>(POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY)
|
||||
if (checkedAddresses.contains(address)) return
|
||||
|
||||
runCatching {
|
||||
do {
|
||||
// Getting batch of extrinsics to check
|
||||
val lastChecked = appPreferencesStore.getObjectMap<Long>(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY)
|
||||
val lastExtrinsic = lastChecked[address]
|
||||
val extrinsicListResult = accountCheckerProvider.getExtrinsicList(afterExtrinsicId = lastExtrinsic)
|
||||
extrinsicListResult.extrinsic
|
||||
?.forEach { tx ->
|
||||
// Checking extrinsic one by one
|
||||
if (checkTx(tx, address, accountCheckerProvider)) return
|
||||
}
|
||||
} while (!extrinsicListResult.extrinsic.isNullOrEmpty())
|
||||
|
||||
// We checked all transactions up to current moment and did not found an `immortal` transaction
|
||||
hasImmortalTransaction.emit(address to false)
|
||||
updateChecked(address, POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY)
|
||||
clearLastCheckedTransaction(address)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkTx(
|
||||
tx: ExtrinsicListItemResponse,
|
||||
address: String,
|
||||
accountCheckerProvider: AccountCheckProvider,
|
||||
): Boolean {
|
||||
val hash = tx.hash
|
||||
val id = tx.id
|
||||
if (hash != null && id != null) {
|
||||
val details = accountCheckerProvider.getExtrinsicDetail(hash)
|
||||
|
||||
// We found an `immortal` transaction
|
||||
if (details.lifetime == null) {
|
||||
hasImmortalTransaction.emit(address to true)
|
||||
updateChecked(address, POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY)
|
||||
clearLastCheckedTransaction(address)
|
||||
return true
|
||||
}
|
||||
|
||||
// Saving last checked transaction
|
||||
updateLastCheckedTransaction(address, id)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun updateLastCheckedTransaction(address: String, txId: Long) {
|
||||
appPreferencesStore.editData {
|
||||
val savedList = it.getObjectMap<Long>(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY)
|
||||
val updatedList = savedList.toMutableMap()
|
||||
updatedList[address] = txId
|
||||
it.setObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY, updatedList)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun clearLastCheckedTransaction(address: String) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val savedList = mutablePreferences.getObjectMap<Long>(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY)
|
||||
val updatedList = savedList.toMutableMap()
|
||||
updatedList.remove(address)
|
||||
mutablePreferences.setObjectMap(POLKADOT_HEALTH_CHECK_LAST_INDEXED_TX_KEY, updatedList)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateChecked(address: String, key: Preferences.Key<String>) {
|
||||
appPreferencesStore.editData { mutablePreferences ->
|
||||
val savedList = mutablePreferences.getObjectSet<String>(key)
|
||||
val updatedList = savedList?.toMutableSet() ?: mutableSetOf()
|
||||
updatedList.add(address)
|
||||
|
||||
if (updatedList.isNotEmpty()) {
|
||||
mutablePreferences.setObjectSet(
|
||||
key,
|
||||
updatedList.toSet(),
|
||||
)
|
||||
} else {
|
||||
mutablePreferences.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ACCOUNT_NOT_FOUND = "Record Not Found"
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import com.tangem.domain.walletmanager.utils.SdkPageConverter
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import timber.log.Timber
|
||||
|
||||
class DefaultTxHistoryRepository(
|
||||
private val cacheRegistry: CacheRegistry,
|
||||
|
|
@ -68,6 +69,7 @@ class DefaultTxHistoryRepository(
|
|||
return pager.flow
|
||||
}
|
||||
|
||||
@Deprecated("Replace with getTxExploreUrl [UserWalletId, Network] instead")
|
||||
override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String {
|
||||
val blockchain = Blockchain.fromId(networkId.value)
|
||||
return when (val txExploreState = blockchain.getExploreTxUrl(txHash)) {
|
||||
|
|
@ -76,22 +78,40 @@ class DefaultTxHistoryRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getTxExploreUrl(userWalletId: UserWalletId, network: Network): String {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = walletManagersFacade.getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
network = network,
|
||||
)
|
||||
val lastTxHash = walletManager?.wallet?.recentTransactions?.last()?.hash.orEmpty()
|
||||
return when (val txExploreState = blockchain?.getExploreTxUrl(lastTxHash)) {
|
||||
is TxExploreState.Url -> txExploreState.url
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFixedSizeTxHistoryItems(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
pageSize: Int,
|
||||
refresh: Boolean,
|
||||
): List<TxHistoryItem> {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getTxHistoryPageKey(currency, userWalletId, Page.Initial),
|
||||
skipCache = refresh,
|
||||
block = { fetchFixedSizeTxHistoryItems(userWalletId, currency, pageSize) },
|
||||
)
|
||||
val txs = txHistoryItemsStore.getSyncOrNull(
|
||||
key = TxHistoryItemsStore.Key(userWalletId, currency),
|
||||
page = Page.Initial,
|
||||
)?.items
|
||||
return txs ?: emptyList()
|
||||
return try {
|
||||
cacheRegistry.invokeOnExpire(
|
||||
key = getTxHistoryPageKey(currency, userWalletId, Page.Initial),
|
||||
skipCache = refresh,
|
||||
block = { fetchFixedSizeTxHistoryItems(userWalletId, currency, pageSize) },
|
||||
)
|
||||
val txs = txHistoryItemsStore.getSyncOrNull(
|
||||
key = TxHistoryItemsStore.Key(userWalletId, currency),
|
||||
page = Page.Initial,
|
||||
)?.items
|
||||
txs ?: emptyList()
|
||||
} catch (e: Throwable) {
|
||||
Timber.e(e, "Unable to load the transaction history for the requested page: ${Page.Initial}")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTxHistoryPageKey(currency: CryptoCurrency, userWalletId: UserWalletId, page: Page): String {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.domain.card
|
|||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
||||
interface ScanCardProcessor {
|
||||
|
|
@ -13,7 +13,7 @@ interface ScanCardProcessor {
|
|||
): CompletionResult<ScanResponse>
|
||||
|
||||
suspend fun scan(
|
||||
analyticsEvent: AnalyticsEvent? = null,
|
||||
analyticsSource: AnalyticsParam.ScreensSources,
|
||||
cardId: String? = null,
|
||||
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
|
||||
onWalletNotCreated: suspend () -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -399,12 +399,27 @@ class DemoConfig {
|
|||
"AB02000000048063",
|
||||
"AB02000000023736",
|
||||
"AB02000000058187",
|
||||
"AB02000000000007",
|
||||
"AC03000000076229",
|
||||
"AF04000000000118",
|
||||
|
||||
// Wallet 2
|
||||
"AF04000000012006",
|
||||
"AF04000000012014",
|
||||
"AF04000000012022",
|
||||
"AF04000000012030",
|
||||
"AF15000000257889",
|
||||
"AF15000000257897",
|
||||
"AF15000000637809",
|
||||
"AF15000000640282",
|
||||
"AF15000001187424",
|
||||
"AF15000001187408",
|
||||
"AF15000001187416",
|
||||
"AF15000001195781",
|
||||
"AF15000001195773",
|
||||
"AF15000001195799",
|
||||
"AF19000000000038",
|
||||
"AC19000000000064",
|
||||
)
|
||||
|
||||
@Suppress("ClassOrdering")
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@ class GetCurrencyWarningsUseCase(
|
|||
coinCurrency = coinStatus.currency,
|
||||
)
|
||||
}
|
||||
feePaidCurrency is FeePaidCurrency.SameCurrency && !tokenStatus.value.amount.isZero() -> {
|
||||
feePaidCurrency is FeePaidCurrency.SameCurrency && tokenStatus.value.amount.isZero() -> {
|
||||
CryptoCurrencyWarning.BalanceNotEnoughForFee(
|
||||
tokenCurrency = tokenStatus.currency,
|
||||
coinCurrency = coinStatus.currency,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetPolkadotCheckHasImmortalUseCase(
|
||||
private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
|
||||
) {
|
||||
operator fun invoke(): Flow<Pair<String, Boolean>> =
|
||||
polkadotAccountHealthCheckRepository.subscribeToHasImmortalResults()
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetPolkadotCheckHasResetUseCase(
|
||||
private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
|
||||
) {
|
||||
|
||||
operator fun invoke(): Flow<Pair<String, Boolean>> =
|
||||
polkadotAccountHealthCheckRepository.subscribeToHasResetResults()
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
class RunPolkadotAccountHealthCheckUseCase(
|
||||
private val polkadotAccountHealthCheckRepository: PolkadotAccountHealthCheckRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<Throwable, Unit> = Either.catch {
|
||||
polkadotAccountHealthCheckRepository.runCheck(userWalletId, network)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PolkadotAccountHealthCheckRepository {
|
||||
|
||||
suspend fun runCheck(userWalletId: UserWalletId, network: Network)
|
||||
|
||||
fun subscribeToHasImmortalResults(): Flow<Pair<String, Boolean>>
|
||||
|
||||
fun subscribeToHasResetResults(): Flow<Pair<String, Boolean>>
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ sealed class SendTransactionError {
|
|||
|
||||
data class DataError(val message: String?) : SendTransactionError()
|
||||
|
||||
data class NetworkError(val message: String?) : SendTransactionError()
|
||||
data class NetworkError(val message: String?, val code: String?) : SendTransactionError()
|
||||
|
||||
data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError()
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,12 @@ class SendTransactionUseCase(
|
|||
}
|
||||
|
||||
private fun handleError(result: SimpleResult.Failure): SendTransactionError {
|
||||
if (ResultChecker.isNetworkError(result)) return SendTransactionError.NetworkError(result.error.message)
|
||||
if (ResultChecker.isNetworkError(result)) {
|
||||
return SendTransactionError.NetworkError(
|
||||
code = result.error.message,
|
||||
message = result.error.customMessage,
|
||||
)
|
||||
}
|
||||
val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError()
|
||||
return when (error) {
|
||||
is BlockchainSdkError.WrappedTangemError -> {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ interface TxHistoryRepository {
|
|||
|
||||
fun getTxExploreUrl(txHash: String, networkId: Network.ID): String
|
||||
|
||||
/** Get transaction url in explorer via last transaction from wallet's recentTransactions list */
|
||||
suspend fun getTxExploreUrl(userWalletId: UserWalletId, network: Network): String
|
||||
|
||||
@Throws(TxHistoryListError::class)
|
||||
suspend fun getFixedSizeTxHistoryItems(
|
||||
userWalletId: UserWalletId,
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import arrow.core.raise.either
|
|||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.txhistory.models.TxStatusError
|
||||
import com.tangem.domain.txhistory.repository.TxHistoryRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
class GetExplorerTransactionUrlUseCase(
|
||||
private val repository: TxHistoryRepository,
|
||||
) {
|
||||
@Deprecated("Replace with invoke [UserWalletId, Network]")
|
||||
operator fun invoke(txHash: String, networkId: Network.ID): Either<TxStatusError, String> {
|
||||
return either {
|
||||
catch(
|
||||
|
|
@ -22,4 +24,17 @@ class GetExplorerTransactionUrlUseCase(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either<TxStatusError, String> {
|
||||
return either {
|
||||
catch(
|
||||
block = {
|
||||
repository.getTxExploreUrl(userWalletId, network).ifEmpty {
|
||||
raise(TxStatusError.EmptyUrlError)
|
||||
}
|
||||
},
|
||||
catch = { raise(TxStatusError.DataError(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,4 +33,15 @@ class GetFixedTxHistoryItemsUseCase(
|
|||
}
|
||||
}.mapLeft { TxHistoryListError.DataError(it) }
|
||||
}
|
||||
|
||||
suspend fun getSync(
|
||||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
pageSize: Int = DEFAULT_PAGE_SIZE,
|
||||
refresh: Boolean = false,
|
||||
): Either<TxHistoryListError, List<TxHistoryItem>> {
|
||||
return Either.catch {
|
||||
repository.getFixedSizeTxHistoryItems(userWalletId, currency, pageSize, refresh)
|
||||
}.mapLeft { TxHistoryListError.DataError(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -84,6 +84,11 @@ interface UserWalletsListManager {
|
|||
*/
|
||||
suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet>
|
||||
|
||||
/**
|
||||
* Indicates that the [UserWalletsListManager] supports [UserWalletsListManager.Lockable]
|
||||
* */
|
||||
fun isLockable(): Boolean
|
||||
|
||||
interface Lockable : UserWalletsListManager {
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,12 +6,6 @@ import com.tangem.domain.wallets.models.UserWallet
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
/**
|
||||
* Indicates that the [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
|
||||
* */
|
||||
val UserWalletsListManager.isLockable: Boolean
|
||||
get() = this is UserWalletsListManager.Lockable
|
||||
|
||||
/**
|
||||
* Indicates that the [UserWalletsListManager] is locked
|
||||
*
|
||||
|
|
@ -48,16 +42,6 @@ suspend fun UserWalletsListManager.unlockIfLockable(type: UnlockType = UnlockTyp
|
|||
return asLockable()?.unlock(type) ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets())
|
||||
}
|
||||
|
||||
/**
|
||||
* Call [UserWalletsListManager.Lockable.lock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable]
|
||||
* or do nothing otherwise
|
||||
*
|
||||
* @see UserWalletsListManager.Lockable.lock
|
||||
* */
|
||||
fun UserWalletsListManager.lockIfLockable() {
|
||||
asLockable()?.lock()
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe cast [UserWalletsListManager] to [UserWalletsListManager.Lockable]
|
||||
*
|
||||
|
|
@ -65,5 +49,8 @@ fun UserWalletsListManager.lockIfLockable() {
|
|||
* [UserWalletsListManager.Lockable] otherwise
|
||||
* */
|
||||
fun UserWalletsListManager.asLockable(): UserWalletsListManager.Lockable? {
|
||||
return this as? UserWalletsListManager.Lockable
|
||||
if (this.isLockable()) {
|
||||
return this as? UserWalletsListManager.Lockable
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ private fun CameraDeniedBottomSheet(content: CameraDeniedBottomSheetConfig) {
|
|||
title = stringResource(id = R.string.qr_scanner_camera_denied_settings_button),
|
||||
icon = R.drawable.ic_settings_24,
|
||||
onItemsClick = {
|
||||
val intent: Intent = Intent(
|
||||
val intent = Intent(
|
||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", context.packageName, null),
|
||||
)
|
||||
|
|
@ -47,7 +47,7 @@ private fun CameraDeniedBottomSheet(content: CameraDeniedBottomSheetConfig) {
|
|||
)
|
||||
SimpleSettingsRow(
|
||||
title = stringResource(id = R.string.common_close),
|
||||
icon = R.drawable.ic_close,
|
||||
icon = R.drawable.ic_close_24,
|
||||
onItemsClick = content.onCancelClick,
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import arrow.core.getOrElse
|
||||
|
|
@ -73,7 +74,8 @@ internal class SendFragment : ComposeFragment() {
|
|||
SystemBarsEffect {
|
||||
setSystemBarsColor(systemBarsColor)
|
||||
}
|
||||
SendScreen(viewModel.uiState, viewModel.stateRouter.currentState)
|
||||
val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle()
|
||||
SendScreen(viewModel.uiState, currentState.value)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
@ -90,9 +92,9 @@ internal class SendFragment : ComposeFragment() {
|
|||
delay(QR_SCAN_DELAY)
|
||||
|
||||
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
|
||||
// If do not use the delay, then etAmount error field is not displayed when
|
||||
// If do not use the delay, then error field is not displayed when
|
||||
// inserting an incorrect amount by shareUri
|
||||
viewModel.onRecipientAddressScanned(it)
|
||||
viewModel.onQrCodeScanned(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
|
||||
data class SendRecipientListContent(
|
||||
val id: String,
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
val title: TextReference = TextReference.EMPTY,
|
||||
val subtitle: TextReference = TextReference.EMPTY,
|
||||
val timestamp: TextReference? = null,
|
||||
val subtitleEndOffset: Int = 0,
|
||||
@DrawableRes val subtitleIconRes: Int? = null,
|
||||
val isVisible: Boolean = true,
|
||||
val isLoading: Boolean = false,
|
||||
)
|
||||
|
|
@ -58,13 +58,22 @@ internal sealed class SendAlertState {
|
|||
override val confirmButtonText: TextReference = resourceReference(R.string.common_continue)
|
||||
}
|
||||
|
||||
data class FeeTooHigh(
|
||||
val times: String,
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : SendAlertState() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference =
|
||||
resourceReference(id = R.string.send_alert_fee_too_high_text, wrappedList(times))
|
||||
override val confirmButtonText: TextReference = resourceReference(R.string.common_continue)
|
||||
}
|
||||
|
||||
data class FeeCoverage(
|
||||
val amount: String,
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SendAlertState() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference =
|
||||
resourceReference(id = R.string.send_alert_fee_coverage_title, wrappedList(amount))
|
||||
resourceReference(id = R.string.send_alert_fee_coverage_title)
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.send_alert_fee_coverage_subract_text)
|
||||
}
|
||||
|
|
@ -75,4 +84,12 @@ internal sealed class SendAlertState {
|
|||
override val message: TextReference =
|
||||
resourceReference(id = R.string.send_notification_invalid_reserve_amount_text)
|
||||
}
|
||||
|
||||
data class FeeUnreachableError(
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SendAlertState() {
|
||||
override val title: TextReference = resourceReference(R.string.send_fee_unreachable_error_title)
|
||||
override val message: TextReference = resourceReference(R.string.send_fee_unreachable_error_text)
|
||||
override val confirmButtonText = resourceReference(R.string.warning_button_refresh)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package com.tangem.features.send.impl.presentation.state
|
|||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory
|
||||
|
|
@ -21,11 +21,15 @@ import java.math.BigDecimal
|
|||
*/
|
||||
internal class SendEventStateFactory(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val feeStateFactory: FeeStateFactory,
|
||||
) {
|
||||
private val sendTransactionErrorConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendTransactionAlertConverter(clickIntents)
|
||||
SendTransactionAlertConverter(
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
}
|
||||
|
||||
fun onConsumeEventState(): SendUiState {
|
||||
|
|
@ -45,18 +49,10 @@ internal class SendEventStateFactory(
|
|||
}
|
||||
|
||||
fun getFeeCoverageAlert(onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amount = state.feeState?.fee?.amount ?: return state
|
||||
val feeAmount = BigDecimalFormatter.formatCryptoAmount(
|
||||
cryptoAmount = amount.value,
|
||||
cryptoCurrency = state.cryptoCurrencySymbol,
|
||||
decimals = amount.decimals,
|
||||
)
|
||||
return state.copy(
|
||||
return currentStateProvider().copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertState.FeeCoverage(
|
||||
amount = feeAmount,
|
||||
onConfirmClick = clickIntents::onSubtractSelect,
|
||||
),
|
||||
),
|
||||
|
|
@ -110,6 +106,20 @@ internal class SendEventStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getFeeTooHighAlert(diff: String, onConsume: () -> Unit): SendUiState {
|
||||
return currentStateProvider().copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertState.FeeTooHigh(
|
||||
onConfirmClick = clickIntents::showSend,
|
||||
times = diff,
|
||||
),
|
||||
),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
|
|
@ -123,4 +133,18 @@ internal class SendEventStateFactory(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getFeeUnreachableErrorState(onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertState.FeeUnreachableError(
|
||||
onConfirmClick = { clickIntents.feeReload(true) },
|
||||
),
|
||||
),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,11 @@ import arrow.core.getOrElse
|
|||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.features.send.impl.R
|
||||
|
|
@ -18,12 +18,14 @@ import com.tangem.features.send.impl.presentation.state.amount.SendAmountSubtrac
|
|||
import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientListConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientHistoryListConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientWalletListConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -33,10 +35,9 @@ internal class SendStateFactory(
|
|||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
private val isTapHelpPreviewEnabledProvider: Provider<Boolean>,
|
||||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
) {
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
|
|
@ -79,9 +80,11 @@ internal class SendStateFactory(
|
|||
isTapHelpPreviewEnabledProvider = isTapHelpPreviewEnabledProvider,
|
||||
)
|
||||
}
|
||||
private val recipientListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendRecipientListConverter(
|
||||
currentStateProvider = currentStateProvider,
|
||||
private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendRecipientWalletListConverter()
|
||||
}
|
||||
private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendRecipientHistoryListConverter(
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
|
@ -92,7 +95,7 @@ internal class SendStateFactory(
|
|||
event = consumedEvent(),
|
||||
isEditingDisabled = false,
|
||||
isBalanceHidden = false,
|
||||
cryptoCurrencySymbol = "",
|
||||
cryptoCurrencyName = "",
|
||||
)
|
||||
|
||||
fun getReadyState(): SendUiState {
|
||||
|
|
@ -103,7 +106,7 @@ internal class SendStateFactory(
|
|||
?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)),
|
||||
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
||||
sendState = confirmStateConverter.convert(Unit),
|
||||
cryptoCurrencySymbol = cryptoCurrencyStatusProvider().currency.symbol,
|
||||
cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +118,7 @@ internal class SendStateFactory(
|
|||
?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)),
|
||||
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
||||
isEditingDisabled = true,
|
||||
cryptoCurrencySymbol = cryptoCurrencyStatusProvider().currency.symbol,
|
||||
cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -125,11 +128,23 @@ internal class SendStateFactory(
|
|||
//endregion
|
||||
|
||||
//region recipient
|
||||
fun onLoadedRecipientList(wallets: List<AvailableWallet?>, txHistory: List<TxHistoryItem>): SendUiState =
|
||||
recipientListStateConverter.convert(
|
||||
wallets = wallets,
|
||||
txHistory = txHistory,
|
||||
fun onLoadedWalletsList(wallets: List<AvailableWallet?>): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
recipientState = state.recipientState?.copy(
|
||||
wallets = recipientWalletListStateConverter.convert(wallets),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onLoadedHistoryList(txHistory: List<TxHistoryItem>): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
recipientState = state.recipientState?.copy(
|
||||
recent = recipientHistoryListStateConverter.convert(txHistory),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
|
|
@ -230,6 +245,22 @@ internal class SendStateFactory(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val recipientState = state.recipientState ?: return state
|
||||
val isNotValid = isAddressInWallet || !isValidAddress
|
||||
return state.copy(
|
||||
recipientState = recipientState.copy(
|
||||
recent = recipientState.recent.map { recent ->
|
||||
recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY))
|
||||
}.toPersistentList(),
|
||||
wallets = recipientState.wallets.map { wallet ->
|
||||
wallet.copy(isVisible = isNotValid && (wallet.isLoading || wallet.title != TextReference.EMPTY))
|
||||
}.toPersistentList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region send
|
||||
|
|
@ -251,14 +282,9 @@ internal class SendStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun getTransactionSendState(txData: TransactionData): SendUiState {
|
||||
fun getTransactionSendState(txData: TransactionData, txUrl: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
val sendState = state.sendState ?: return state
|
||||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
txHash = txData.hash.orEmpty(),
|
||||
networkId = cryptoCurrency.network.id,
|
||||
).getOrElse { "" }
|
||||
return state.copy(
|
||||
sendState = sendState.copy(
|
||||
transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendTransactionAlertConverter(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
) : Converter<SendTransactionError, SendAlertState?> {
|
||||
override fun convert(value: SendTransactionError): SendAlertState? {
|
||||
|
|
@ -29,8 +33,8 @@ internal class SendTransactionAlertConverter(
|
|||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) },
|
||||
)
|
||||
is SendTransactionError.NetworkError -> SendAlertState.TransactionError(
|
||||
code = "",
|
||||
cause = value.message,
|
||||
code = value.code.orEmpty(),
|
||||
cause = value.message.orEmpty(),
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.message.orEmpty()) },
|
||||
)
|
||||
is SendTransactionError.UnknownError -> SendAlertState.TransactionError(
|
||||
|
|
@ -38,7 +42,12 @@ internal class SendTransactionAlertConverter(
|
|||
cause = value.ex?.localizedMessage,
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) },
|
||||
)
|
||||
is SendTransactionError.CreateAccountUnderfunded -> SendAlertState.ReserveAmount(value.amount)
|
||||
is SendTransactionError.CreateAccountUnderfunded -> SendAlertState.ReserveAmount(
|
||||
BigDecimalFormatter.formatWithSymbol(
|
||||
amount = value.amount,
|
||||
symbol = cryptoCurrencyStatusProvider().currency.symbol,
|
||||
),
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import java.math.BigDecimal
|
|||
internal data class SendUiState(
|
||||
val clickIntents: SendClickIntents,
|
||||
val isEditingDisabled: Boolean,
|
||||
val cryptoCurrencySymbol: String,
|
||||
val cryptoCurrencyName: String,
|
||||
val amountState: SendStates.AmountState? = null,
|
||||
val recipientState: SendStates.RecipientState? = null,
|
||||
val feeState: SendStates.FeeState? = null,
|
||||
|
|
@ -52,6 +52,7 @@ internal sealed class SendStates {
|
|||
val notifications: ImmutableList<SendNotification>,
|
||||
val appCurrencyCode: String,
|
||||
val isFeeLoading: Boolean,
|
||||
val subtractedFee: BigDecimal?,
|
||||
) : SendStates()
|
||||
|
||||
/** Recipient state */
|
||||
|
|
@ -77,6 +78,7 @@ internal sealed class SendStates {
|
|||
val rate: BigDecimal?,
|
||||
val appCurrency: AppCurrency,
|
||||
val isFeeApproximate: Boolean,
|
||||
val isCustomSelected: Boolean,
|
||||
val notifications: ImmutableList<SendNotification>,
|
||||
) : SendStates()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotification
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class AmountNotificationFactory(
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
) {
|
||||
|
||||
fun create() = stateRouterProvider().currentState
|
||||
.filter { it.type == SendUiStateType.Amount }
|
||||
.map {
|
||||
buildList {
|
||||
addFeeUnreachableNotification()
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addFeeUnreachableNotification() {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return
|
||||
|
||||
if (feeState.feeSelectorState is FeeSelectorState.Error) {
|
||||
add(
|
||||
SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ internal class SendAmountStateConverter(
|
|||
notifications = persistentListOf(),
|
||||
isFeeLoading = false,
|
||||
appCurrencyCode = appCurrency.code,
|
||||
subtractedFee = null,
|
||||
segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) {
|
||||
persistentListOf()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -15,14 +15,16 @@ internal class SendAmountSubtractConverter(
|
|||
val state = currentStateProvider()
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val amountState = state.amountState ?: return state
|
||||
val feeValue = state.feeState?.fee?.amount?.value ?: return state
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeValue = feeState.fee?.amount?.value ?: return state
|
||||
val amountTextField = amountState.amountTextField
|
||||
val amountValue = amountTextField.cryptoAmount.value ?: return state
|
||||
val fiatRate = cryptoCurrencyStatus.value.fiatRate
|
||||
val cryptoDecimals = amountTextField.cryptoAmount.decimals
|
||||
val fiatDecimals = amountTextField.fiatAmount.decimals
|
||||
|
||||
val decimalCryptoValue = amountValue.minus(feeValue)
|
||||
val feeDiff = amountState.subtractedFee?.let { feeValue.minus(it) } ?: feeValue
|
||||
val decimalCryptoValue = amountValue.minus(feeDiff)
|
||||
|
||||
if (decimalCryptoValue < BigDecimal.ZERO) return state
|
||||
|
||||
|
|
@ -33,6 +35,7 @@ internal class SendAmountSubtractConverter(
|
|||
return state.copy(
|
||||
sendState = state.sendState?.copy(isSubtract = true),
|
||||
amountState = amountState.copy(
|
||||
subtractedFee = feeValue,
|
||||
amountTextField = amountTextField.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.blockchainsdk.utils.minimalAmount
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.extensions.networkIconResId
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
|
|
@ -20,6 +21,7 @@ import com.tangem.features.send.impl.presentation.state.*
|
|||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isDogecoin
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -29,11 +31,13 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SendNotificationFactory(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feePaidCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val currencyChecksRepository: CurrencyChecksRepository,
|
||||
|
|
@ -51,18 +55,17 @@ internal class SendNotificationFactory(
|
|||
val feeState = state.feeState ?: return@map persistentListOf()
|
||||
val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO
|
||||
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
|
||||
val sendAmount = if (sendState.isSubtract) amountValue.minus(feeAmount) else amountValue
|
||||
buildList {
|
||||
// errors
|
||||
addExceedBalanceNotification(feeAmount, sendAmount)
|
||||
addExceedBalanceNotification(feeAmount, amountValue)
|
||||
addExceedsBalanceNotification(feeState.fee)
|
||||
addInvalidAmountNotification(sendState.isSubtract, sendAmount)
|
||||
addMinimumAmountErrorNotification(feeAmount, sendAmount)
|
||||
addDustWarningNotification(feeAmount, sendAmount)
|
||||
addTransactionLimitErrorNotification(feeAmount, sendAmount)
|
||||
addMinimumAmountErrorNotification(feeAmount, amountValue)
|
||||
addDustWarningNotification(feeAmount, amountValue)
|
||||
addTransactionLimitErrorNotification(feeAmount, amountValue)
|
||||
// warnings
|
||||
addExistentialWarningNotification(feeAmount, sendAmount)
|
||||
addHighFeeWarningNotification(sendAmount, sendState.ignoreAmountReduce)
|
||||
addExistentialWarningNotification(feeAmount, amountValue)
|
||||
addHighFeeWarningNotification(feeAmount, amountValue, sendState.ignoreAmountReduce)
|
||||
addTooHighNotification(feeState.feeSelectorState)
|
||||
addTooLowNotification(feeState)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
|
@ -87,10 +90,11 @@ internal class SendNotificationFactory(
|
|||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider()
|
||||
val feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatusProvider()
|
||||
val cryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val coinCryptoAmount = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
|
||||
val showNotification = if (cryptoCurrencyStatus.currency is CryptoCurrency.Token) {
|
||||
val showNotification = if (cryptoCurrencyStatus.currency.id == feePaidCryptoCurrencyStatus?.currency?.id) {
|
||||
receivedAmount > cryptoAmount || feeAmount > coinCryptoAmount
|
||||
} else {
|
||||
receivedAmount + feeAmount > cryptoAmount
|
||||
|
|
@ -101,15 +105,6 @@ internal class SendNotificationFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addInvalidAmountNotification(
|
||||
isSubtractAmount: Boolean,
|
||||
receivedAmount: BigDecimal,
|
||||
) {
|
||||
if (isSubtractAmount && receivedAmount <= BigDecimal.ZERO) {
|
||||
add(SendNotification.Error.InvalidAmount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addMinimumAmountErrorNotification(
|
||||
feeAmount: BigDecimal,
|
||||
receivedAmount: BigDecimal,
|
||||
|
|
@ -119,20 +114,11 @@ internal class SendNotificationFactory(
|
|||
val totalAmount = feeAmount + receivedAmount
|
||||
val balance = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
|
||||
// TODO Move Blockchain check elsewhere
|
||||
when (coinCryptoCurrencyStatus.currency.network.id.value) {
|
||||
Blockchain.Cardano.id -> {
|
||||
if (receivedAmount > BigDecimal.ONE || balance - totalAmount < BigDecimal.ONE) {
|
||||
add(SendNotification.Error.MinimumAmountError(CARDANO_MINIMUM))
|
||||
}
|
||||
if (isDogecoin(coinCryptoCurrencyStatus.currency.network.id.value)) {
|
||||
val minimum = BigDecimal(DOGECOIN_MINIMUM)
|
||||
if (receivedAmount < minimum || balance - totalAmount < minimum) {
|
||||
add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM))
|
||||
}
|
||||
Blockchain.Dogecoin.id -> {
|
||||
val minimum = BigDecimal(DOGECOIN_MINIMUM)
|
||||
if (receivedAmount > minimum || balance - totalAmount < minimum) {
|
||||
add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM))
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,9 +167,8 @@ internal class SendNotificationFactory(
|
|||
cryptoCurrency = cryptoCurrency,
|
||||
),
|
||||
onConfirmClick = {
|
||||
val reduceTo = utxoLimit.maxAmount.toPlainString()
|
||||
clickIntents.onAmountReduceClick(
|
||||
reduceTo,
|
||||
utxoLimit.maxAmount,
|
||||
SendNotification.Error.TransactionLimitError::class.java,
|
||||
)
|
||||
},
|
||||
|
|
@ -197,7 +182,9 @@ internal class SendNotificationFactory(
|
|||
receivedAmount: BigDecimal,
|
||||
) {
|
||||
val userWalletId = userWalletProvider().walletId
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: return
|
||||
val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) {
|
||||
feeAmount
|
||||
} else {
|
||||
|
|
@ -207,7 +194,8 @@ internal class SendNotificationFactory(
|
|||
userWalletId,
|
||||
cryptoCurrency.network,
|
||||
)
|
||||
if (currencyDeposit != null && currencyDeposit > spendingAmount) {
|
||||
val diff = balance.minus(spendingAmount)
|
||||
if (currencyDeposit != null && currencyDeposit > diff) {
|
||||
add(
|
||||
SendNotification.Error.ExistentialDeposit(
|
||||
BigDecimalFormatter.formatCryptoAmount(
|
||||
|
|
@ -220,18 +208,21 @@ internal class SendNotificationFactory(
|
|||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addHighFeeWarningNotification(
|
||||
feeAmount: BigDecimal,
|
||||
sendAmount: BigDecimal,
|
||||
ignoreAmountReduce: Boolean,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id
|
||||
if (!ignoreAmountReduce && sendAmount == balance && isTezos) {
|
||||
val threshold = Blockchain.Tezos.minimalAmount()
|
||||
val isTotalBalance = feeAmount.plus(sendAmount) >= balance && balance > threshold
|
||||
if (!ignoreAmountReduce && isTotalBalance && isTezos) {
|
||||
add(
|
||||
SendNotification.Warning.HighFeeError(
|
||||
amount = TEZOS_FEE_THRESHOLD.toPlainString(),
|
||||
amount = threshold.toPlainString(),
|
||||
onConfirmClick = {
|
||||
val reduceTo = sendAmount.minus(TEZOS_FEE_THRESHOLD).toPlainString()
|
||||
val reduceTo = sendAmount.minus(threshold)
|
||||
clickIntents.onAmountReduceClick(reduceTo, SendNotification.Warning.HighFeeError::class.java)
|
||||
},
|
||||
onCloseClick = {
|
||||
|
|
@ -281,6 +272,18 @@ internal class SendNotificationFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addTooHighNotification(feeSelectorState: FeeSelectorState) {
|
||||
if (feeSelectorState !is FeeSelectorState.Content) return
|
||||
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return
|
||||
val highValue = multipleFees.priority.amount.value ?: return
|
||||
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
val diff = (customValue / highValue).toBigInteger()
|
||||
if (feeSelectorState.selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF) {
|
||||
add(SendNotification.Warning.TooHigh(diff.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<SendNotification>.addExceedsBalanceNotification(fee: Fee?) {
|
||||
val feeValue = fee?.amount?.value ?: BigDecimal.ZERO
|
||||
val userWalletId = userWalletProvider().walletId
|
||||
|
|
@ -359,8 +362,7 @@ internal class SendNotificationFactory(
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val CARDANO_MINIMUM = "1"
|
||||
private const val DOGECOIN_MINIMUM = "0.01"
|
||||
private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01")
|
||||
internal val FEE_MAX_DIFF = BigInteger("5")
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee
|
|||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory
|
||||
|
||||
/**
|
||||
* Check if sending amount with fee is greater than balance
|
||||
|
|
@ -12,7 +13,7 @@ internal fun checkFeeCoverage(state: SendUiState, cryptoCurrencyStatus: CryptoCu
|
|||
val balance = cryptoCurrencyStatus.value.amount ?: return false
|
||||
val fee = state.feeState?.fee?.amount?.value ?: return false
|
||||
val amount = state.amountState?.amountTextField?.cryptoAmount?.value ?: return false
|
||||
return balance <= amount + fee
|
||||
return balance < amount + fee && balance > fee
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -26,4 +27,19 @@ internal fun checkIfFeeTooLow(state: SendUiState): Boolean {
|
|||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
|
||||
return feeSelectorState.selectedFee == FeeType.Custom && minimumValue > customValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if custom fee is too high
|
||||
*/
|
||||
internal fun checkIfFeeTooHigh(state: SendUiState, onShow: (String) -> Unit): Boolean {
|
||||
val feeSelectorState = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
|
||||
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return false
|
||||
val highValue = multipleFees.priority.amount.value ?: return false
|
||||
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
val diff = (customValue / highValue).toBigInteger()
|
||||
val isShow = feeSelectorState.selectedFee == FeeType.Custom && diff > SendNotificationFactory.FEE_MAX_DIFF
|
||||
if (isShow) onShow(diff.toString())
|
||||
return isShow
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee
|
|||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
|
|
@ -13,10 +14,10 @@ import com.tangem.utils.converter.Converter
|
|||
internal class FeeConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : Converter<FeeSelectorState.Content, Fee> {
|
||||
|
||||
private val ethereumCustomFeeConverter by lazy {
|
||||
private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
EthereumCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
|
|
@ -24,6 +25,14 @@ internal class FeeConverter(
|
|||
)
|
||||
}
|
||||
|
||||
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BitcoinCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: FeeSelectorState.Content): Fee {
|
||||
return when (val fees = value.fees) {
|
||||
is TransactionFee.Choosable -> {
|
||||
|
|
@ -46,6 +55,7 @@ internal class FeeConverter(
|
|||
} else {
|
||||
when (normalFee) {
|
||||
is Fee.Ethereum -> ethereumCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues)
|
||||
is Fee.Bitcoin -> bitcoinCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues)
|
||||
else -> {
|
||||
val customFee = customValues.firstOrNull()
|
||||
Fee.Common(
|
||||
|
|
|
|||
|
|
@ -1,20 +1,15 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotification
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.state.SendNotification
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.toFormattedString
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeeNotificationFactory(
|
||||
|
|
@ -29,16 +24,7 @@ internal class FeeNotificationFactory(
|
|||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return@map persistentListOf()
|
||||
buildList {
|
||||
when (val feeSelectorState = feeState.feeSelectorState) {
|
||||
FeeSelectorState.Error -> {
|
||||
addFeeUnreachableNotification(feeSelectorState)
|
||||
}
|
||||
is FeeSelectorState.Content -> {
|
||||
val customFee = feeSelectorState.customValues
|
||||
val selectedFee = feeSelectorState.selectedFee
|
||||
addTooHighNotification(feeSelectorState.fees, selectedFee, customFee)
|
||||
}
|
||||
}
|
||||
addFeeUnreachableNotification(feeState.feeSelectorState)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
|
|
@ -47,24 +33,4 @@ internal class FeeNotificationFactory(
|
|||
add(SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendNotification>.addTooHighNotification(
|
||||
transactionFee: TransactionFee,
|
||||
selectedFee: FeeType,
|
||||
customFee: List<SendTextField.CustomFee>,
|
||||
) {
|
||||
val multipleFees = transactionFee as? TransactionFee.Choosable ?: return
|
||||
val highValue = multipleFees.priority.amount.value ?: return
|
||||
val customAmount = customFee.firstOrNull() ?: return
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
val diff = customValue / highValue
|
||||
if (selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF) {
|
||||
add(SendNotification.Warning.TooHigh(diff.toFormattedString(HIGH_FEE_DIFF_DECIMALS)))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val FEE_MAX_DIFF = BigDecimal(5)
|
||||
private const val HIGH_FEE_DIFF_DECIMALS = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
internal class FeeStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
) {
|
||||
|
|
@ -56,18 +56,26 @@ internal class FeeStateFactory(
|
|||
fun onFeeOnLoadedState(fees: TransactionFee): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.feeState ?: return state
|
||||
val feeSelectorState = (feeState.feeSelectorState as? FeeSelectorState.Content)?.copy(
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content
|
||||
|
||||
val isCustomWasSelected = if (feeState.isCustomSelected) {
|
||||
feeSelectorState?.customValues ?: persistentListOf()
|
||||
} else {
|
||||
customFeeFieldConverter.convert(fees.normal)
|
||||
}
|
||||
val updatedFeeSelectorState = feeSelectorState?.copy(
|
||||
fees = fees,
|
||||
customValues = isCustomWasSelected,
|
||||
) ?: FeeSelectorState.Content(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
|
||||
val fee = feeConverter.convert(feeSelectorState)
|
||||
val fee = feeConverter.convert(updatedFeeSelectorState)
|
||||
return state.copy(
|
||||
amountState = state.amountState?.copy(isFeeLoading = false),
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = feeSelectorState,
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
fee = fee,
|
||||
isFeeApproximate = isFeeApproximate(fee),
|
||||
),
|
||||
|
|
@ -91,9 +99,11 @@ internal class FeeStateFactory(
|
|||
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType)
|
||||
val fee = feeConverter.convert(updatedFeeSelectorState)
|
||||
val isCustomFeeWasSelected = feeState.isCustomSelected || updatedFeeSelectorState.selectedFee == FeeType.Custom
|
||||
return state.copy(
|
||||
feeState = feeState.copy(
|
||||
fee = fee,
|
||||
isCustomSelected = isCustomFeeWasSelected,
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
),
|
||||
)
|
||||
|
|
@ -143,7 +153,7 @@ internal class FeeStateFactory(
|
|||
}
|
||||
|
||||
private fun isFeeApproximate(fee: Fee): Boolean {
|
||||
val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider()
|
||||
val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return false
|
||||
return isFeeApproximateUseCase(
|
||||
networkId = cryptoCurrencyStatus.currency.network.id,
|
||||
amountType = fee.amount.type,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.state.fee
|
|||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
|
@ -14,10 +15,10 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
internal class SendFeeCustomFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : Converter<Fee, ImmutableList<SendTextField.CustomFee>> {
|
||||
|
||||
private val ethereumCustomFeeConverter by lazy {
|
||||
private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
EthereumCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
|
|
@ -25,22 +26,35 @@ internal class SendFeeCustomFieldConverter(
|
|||
)
|
||||
}
|
||||
|
||||
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BitcoinCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: Fee): ImmutableList<SendTextField.CustomFee> {
|
||||
return when (value) {
|
||||
is Fee.Ethereum -> {
|
||||
ethereumCustomFeeConverter.convert(value)
|
||||
}
|
||||
is Fee.Ethereum -> ethereumCustomFeeConverter.convert(value)
|
||||
is Fee.Bitcoin -> bitcoinCustomFeeConverter.convert(value)
|
||||
else -> persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun onValueChange(feeSelectorState: FeeSelectorState.Content, index: Int, value: String) = feeSelectorState.copy(
|
||||
customValues = when (feeSelectorState.fees.normal) {
|
||||
customValues = when (val fee = feeSelectorState.fees.normal) {
|
||||
is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange(
|
||||
customValues = feeSelectorState.customValues,
|
||||
index = index,
|
||||
value = value,
|
||||
)
|
||||
is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange(
|
||||
customValues = feeSelectorState.customValues,
|
||||
index = index,
|
||||
value = value,
|
||||
txSize = fee.txSize,
|
||||
)
|
||||
else -> feeSelectorState.customValues
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
|
||||
internal class SendFeeStateConverter(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : Converter<Unit, SendStates.FeeState> {
|
||||
|
||||
override fun convert(value: Unit): SendStates.FeeState {
|
||||
|
|
@ -17,9 +17,10 @@ internal class SendFeeStateConverter(
|
|||
feeSelectorState = FeeSelectorState.Error,
|
||||
fee = null,
|
||||
notifications = persistentListOf(),
|
||||
rate = feeCryptoCurrencyStatusProvider().value.fiatRate,
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
isFeeApproximate = false,
|
||||
isCustomSelected = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee.custom
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class BitcoinCustomFeeConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : CustomFeeConverter<Fee.Bitcoin> {
|
||||
|
||||
override fun convert(value: Fee.Bitcoin): ImmutableList<SendTextField.CustomFee> {
|
||||
val feeValue = value.amount.value
|
||||
val network = feeCryptoCurrencyStatusProvider()?.currency?.network?.id?.value
|
||||
return if (network != null && isBitcoin(network)) {
|
||||
persistentListOf(
|
||||
SendTextField.CustomFee(
|
||||
value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(),
|
||||
decimals = value.amount.decimals,
|
||||
symbol = value.amount.currencySymbol,
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
title = resourceReference(R.string.send_max_fee),
|
||||
footer = resourceReference(R.string.send_max_fee_footer),
|
||||
label = getFeeFormatted(feeValue),
|
||||
keyboardActions = KeyboardActions(),
|
||||
isReadonly = true,
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = toSatoshiPerByte(
|
||||
amount = feeValue,
|
||||
decimals = value.amount.decimals,
|
||||
txSize = value.txSize,
|
||||
).toString(),
|
||||
decimals = SATOSHI_DECIMALS,
|
||||
symbol = "",
|
||||
title = resourceReference(R.string.send_satoshi_per_byte_title),
|
||||
footer = resourceReference(R.string.send_satoshi_per_byte_text),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (checkExceedBalance(feeValue)) ImeAction.None else ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(normalFee: Fee.Bitcoin, value: ImmutableList<SendTextField.CustomFee>): Fee.Bitcoin {
|
||||
val feeAmount = value[FEE_AMOUNT_INDEX].value.parseToBigDecimal(value[FEE_AMOUNT_INDEX].decimals)
|
||||
val satoshiPerByte = value[FEE_SATOSHI_INDEX].value.parseToBigDecimal(value[FEE_SATOSHI_INDEX].decimals)
|
||||
return normalFee.copy(
|
||||
amount = normalFee.amount.copy(value = feeAmount),
|
||||
satoshiPerByte = satoshiPerByte,
|
||||
)
|
||||
}
|
||||
|
||||
fun onValueChange(
|
||||
customValues: ImmutableList<SendTextField.CustomFee>,
|
||||
index: Int,
|
||||
value: String,
|
||||
txSize: BigDecimal,
|
||||
): ImmutableList<SendTextField.CustomFee> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
return mutableCustomValues.apply {
|
||||
if (index == FEE_SATOSHI_INDEX) {
|
||||
val newSatoshiPerKb = value.parseToBigDecimal(this[FEE_SATOSHI_INDEX].decimals)
|
||||
val newFeeAmount = newSatoshiPerKb.multiply(txSize)
|
||||
.movePointLeft(this[FEE_AMOUNT_INDEX].decimals)
|
||||
.setScale(this[FEE_AMOUNT_INDEX].decimals, RoundingMode.DOWN)
|
||||
set(
|
||||
FEE_AMOUNT_INDEX,
|
||||
this[FEE_AMOUNT_INDEX].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT_INDEX].decimals),
|
||||
label = getFeeFormatted(newFeeAmount),
|
||||
),
|
||||
)
|
||||
set(index, this[index].copy(value = value))
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun getFeeFormatted(fee: BigDecimal?): TextReference {
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate
|
||||
val fiatFee = rate?.let { fee?.multiply(it) }
|
||||
return stringReference(
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = fiatFee,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean {
|
||||
val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider()
|
||||
val currencyCryptoAmount = cryptoCurrencyStatus?.value?.amount ?: BigDecimal.ZERO
|
||||
|
||||
return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount
|
||||
}
|
||||
|
||||
private fun toSatoshiPerByte(amount: BigDecimal?, decimals: Int, txSize: BigDecimal): BigDecimal? {
|
||||
val newFeeAmount = amount?.movePointRight(decimals)
|
||||
return newFeeAmount?.divide(
|
||||
txSize,
|
||||
SATOSHI_DECIMALS,
|
||||
RoundingMode.HALF_UP,
|
||||
)?.setScale(SATOSHI_DECIMALS, RoundingMode.HALF_UP)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val FEE_AMOUNT_INDEX = 0
|
||||
private const val FEE_SATOSHI_INDEX = 1
|
||||
private const val SATOSHI_DECIMALS = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee.custom
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal interface CustomFeeConverter<T : Fee> : Converter<T, ImmutableList<SendTextField.CustomFee>> {
|
||||
fun convertBack(normalFee: T, value: ImmutableList<SendTextField.CustomFee>): T
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ import com.tangem.features.send.impl.R
|
|||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -28,8 +27,8 @@ import java.math.RoundingMode
|
|||
internal class EthereumCustomFeeConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Fee.Ethereum, ImmutableList<SendTextField.CustomFee>> {
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : CustomFeeConverter<Fee.Ethereum> {
|
||||
|
||||
override fun convert(value: Fee.Ethereum): ImmutableList<SendTextField.CustomFee> {
|
||||
val feeValue = value.amount.value
|
||||
|
|
@ -49,8 +48,8 @@ internal class EthereumCustomFeeConverter(
|
|||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = value.gasPrice.toString(),
|
||||
decimals = GAS_DECIMALS,
|
||||
value = value.gasPrice.toBigDecimal().movePointLeft(GIGA_DECIMALS).parseBigDecimal(GIGA_DECIMALS),
|
||||
decimals = GIGA_DECIMALS,
|
||||
symbol = ETHEREUM_GAS_UNIT,
|
||||
title = resourceReference(R.string.send_gas_price),
|
||||
footer = resourceReference(R.string.send_gas_price_footer),
|
||||
|
|
@ -77,7 +76,7 @@ internal class EthereumCustomFeeConverter(
|
|||
)
|
||||
}
|
||||
|
||||
fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList<SendTextField.CustomFee>): Fee.Ethereum {
|
||||
override fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList<SendTextField.CustomFee>): Fee.Ethereum {
|
||||
val feeAmount = value[FEE_AMOUNT].value.parseToBigDecimal(value[FEE_AMOUNT].decimals)
|
||||
val gasPrice = value[GAS_PRICE].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger()
|
||||
val gasLimit = value[GAS_LIMIT].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger()
|
||||
|
|
@ -105,7 +104,7 @@ internal class EthereumCustomFeeConverter(
|
|||
|
||||
private fun getFeeFormatted(fee: BigDecimal?): TextReference {
|
||||
val appCurrency = appCurrencyProvider()
|
||||
val rate = feeCryptoCurrencyStatusProvider().value.fiatRate
|
||||
val rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate
|
||||
val fiatFee = rate?.let { fee?.multiply(it) }
|
||||
return stringReference(
|
||||
BigDecimalFormatter.formatFiatAmount(
|
||||
|
|
@ -118,7 +117,7 @@ internal class EthereumCustomFeeConverter(
|
|||
|
||||
private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean {
|
||||
val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider()
|
||||
val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val currencyCryptoAmount = cryptoCurrencyStatus?.value?.amount ?: BigDecimal.ZERO
|
||||
|
||||
return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount
|
||||
}
|
||||
|
|
@ -134,9 +133,9 @@ internal class EthereumCustomFeeConverter(
|
|||
setEmpty(GAS_PRICE)
|
||||
} else {
|
||||
val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals)
|
||||
val newFeeAmount = newFeeAmountDecimal.movePointRight(this[FEE_AMOUNT].decimals)
|
||||
val newGasPrice = newFeeAmount.divide(gasLimit, GAS_DECIMALS, RoundingMode.HALF_UP)
|
||||
set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(GAS_DECIMALS)))
|
||||
val newFeeAmount = newFeeAmountDecimal.movePointRight(this[GAS_PRICE].decimals) // from ETH to GWEI
|
||||
val newGasPrice = newFeeAmount.divide(gasLimit, this[GAS_PRICE].decimals, RoundingMode.HALF_UP)
|
||||
set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(this[GAS_PRICE].decimals)))
|
||||
set(
|
||||
index,
|
||||
this[index].copy(
|
||||
|
|
@ -154,8 +153,8 @@ internal class EthereumCustomFeeConverter(
|
|||
setEmpty(GAS_PRICE)
|
||||
} else {
|
||||
val newGasPrice = value.parseToBigDecimal(this[GAS_PRICE].decimals)
|
||||
.movePointLeft(this[GAS_PRICE].decimals)
|
||||
val newFeeAmount = (gasLimit * newGasPrice).movePointLeft(this[FEE_AMOUNT].decimals)
|
||||
.movePointLeft(this[GAS_PRICE].decimals) // from GWEI to ETH
|
||||
val newFeeAmount = gasLimit * newGasPrice
|
||||
set(
|
||||
FEE_AMOUNT,
|
||||
this[FEE_AMOUNT].copy(
|
||||
|
|
@ -174,7 +173,7 @@ internal class EthereumCustomFeeConverter(
|
|||
} else {
|
||||
val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
val gasPrice = this[GAS_PRICE].value.parseToBigDecimal(this[GAS_PRICE].decimals)
|
||||
.movePointLeft(this[FEE_AMOUNT].decimals)
|
||||
.movePointLeft(this[GAS_PRICE].decimals) // from GWEI to ETH
|
||||
val newFeeAmount = newGasLimit * gasPrice
|
||||
set(
|
||||
FEE_AMOUNT,
|
||||
|
|
@ -198,6 +197,7 @@ internal class EthereumCustomFeeConverter(
|
|||
|
||||
companion object {
|
||||
private const val ETHEREUM_GAS_UNIT = "GWEI"
|
||||
private const val GIGA_DECIMALS = 9
|
||||
private const val FEE_AMOUNT = 0
|
||||
private const val GAS_PRICE = 1
|
||||
private const val GAS_LIMIT = 2
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ internal class SendAmountFieldChangeConverter(
|
|||
return state.copy(
|
||||
amountState = amountState.copy(
|
||||
isPrimaryButtonEnabled = !isExceedBalance && !isZero,
|
||||
subtractedFee = null,
|
||||
amountTextField = amountTextField.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
|
|
@ -45,7 +46,7 @@ internal class SendAmountFieldChangeConverter(
|
|||
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
|
||||
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (!isExceedBalance) ImeAction.Done else ImeAction.None,
|
||||
imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue),
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
|
|
@ -103,4 +104,11 @@ internal class SendAmountFieldChangeConverter(
|
|||
cryptoDecimal > currencyCryptoAmount
|
||||
}
|
||||
}
|
||||
|
||||
private fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) =
|
||||
if (!isExceedBalance && !decimalCryptoValue.isZero()) {
|
||||
ImeAction.Done
|
||||
} else {
|
||||
ImeAction.None
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ internal class SendAmountFieldMaxAmountConverter(
|
|||
return state.copy(
|
||||
amountState = amountState.copy(
|
||||
isPrimaryButtonEnabled = true,
|
||||
subtractedFee = null,
|
||||
amountTextField = amountTextField.copy(
|
||||
value = cryptoValue,
|
||||
fiatValue = fiatValue,
|
||||
|
|
|
|||
|
|
@ -64,5 +64,6 @@ internal sealed class SendTextField {
|
|||
val title: TextReference,
|
||||
val footer: TextReference,
|
||||
val label: TextReference? = null,
|
||||
val isReadonly: Boolean = false,
|
||||
) : SendTextField()
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ internal object AmountStatePreviewData {
|
|||
notifications = persistentListOf(),
|
||||
isFeeLoading = false,
|
||||
appCurrencyCode = "usd",
|
||||
subtractedFee = null,
|
||||
amountTextField = SendTextField.AmountField(
|
||||
value = "123.123123123123123123",
|
||||
onValueChange = {},
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ internal object FeeStatePreviewData {
|
|||
),
|
||||
isFeeApproximate = false,
|
||||
notifications = persistentListOf(),
|
||||
isCustomSelected = false,
|
||||
)
|
||||
|
||||
val errorFeeState = feeState.copy(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
|
|||
import com.tangem.features.send.impl.presentation.state.SendNotification
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
internal object SendClickIntentsStub : SendClickIntents {
|
||||
|
|
@ -33,7 +34,7 @@ internal object SendClickIntentsStub : SendClickIntents {
|
|||
|
||||
override fun onRecipientMemoValueChange(value: String) {}
|
||||
|
||||
override fun feeReload() {}
|
||||
override fun feeReload(isToNextState: Boolean) {}
|
||||
|
||||
override fun onFeeSelectorClick(feeType: FeeType) {}
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ internal object SendClickIntentsStub : SendClickIntents {
|
|||
|
||||
override fun onShareClick() {}
|
||||
|
||||
override fun onAmountReduceClick(reducedAmount: String, clazz: Class<out SendNotification>) {}
|
||||
override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class<out SendNotification>) {}
|
||||
|
||||
override fun onNotificationCancel(clazz: Class<out SendNotification>) {}
|
||||
}
|
||||
|
|
@ -10,49 +10,26 @@ import com.tangem.domain.tokens.model.CryptoCurrency
|
|||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.RECENT_DEFAULT_COUNT
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.RECENT_KEY_TAG
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.toFormattedCurrencyString
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SendRecipientListConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
internal class SendRecipientHistoryListConverter(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) {
|
||||
) : Converter<List<TxHistoryItem>, ImmutableList<SendRecipientListContent>> {
|
||||
|
||||
fun convert(wallets: List<AvailableWallet?>, txHistory: List<TxHistoryItem>): SendUiState {
|
||||
override fun convert(value: List<TxHistoryItem>): ImmutableList<SendRecipientListContent> {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
val state = currentStateProvider()
|
||||
val recipientState = state.recipientState ?: return state
|
||||
|
||||
return state.copy(
|
||||
recipientState = recipientState.copy(
|
||||
wallets = wallets.filterWallets(),
|
||||
recent = txHistory.filterRecipients(cryptoCurrency),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<AvailableWallet?>.filterWallets() = this.filterNotNull()
|
||||
.groupBy { item -> item.name }
|
||||
.values.map {
|
||||
it.mapIndexed { index, item ->
|
||||
val name = if (it.size > 1) {
|
||||
"${item.name} ${index.inc()}"
|
||||
} else {
|
||||
item.name
|
||||
}
|
||||
SendRecipientListContent(
|
||||
id = item.address,
|
||||
title = TextReference.Str(item.address),
|
||||
subtitle = TextReference.Str(name),
|
||||
)
|
||||
}
|
||||
return value.filterRecipients(cryptoCurrency).ifEmpty {
|
||||
emptyListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT)
|
||||
}
|
||||
.flatten()
|
||||
.toPersistentList()
|
||||
}
|
||||
|
||||
private fun List<TxHistoryItem>.filterRecipients(cryptoCurrency: CryptoCurrency) = this.filter { item ->
|
||||
val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer
|
||||
|
|
@ -65,9 +42,9 @@ internal class SendRecipientListConverter(
|
|||
isTransfer && isSingleAddress && isNotContract
|
||||
}
|
||||
.take(RECENT_LIST_SIZE)
|
||||
.map { tx ->
|
||||
.mapIndexed { index, tx ->
|
||||
SendRecipientListContent(
|
||||
id = tx.txHash,
|
||||
id = "$RECENT_KEY_TAG$index",
|
||||
title = tx.extractAddress(),
|
||||
subtitle = stringReference(tx.getAmount(cryptoCurrency).trim()),
|
||||
timestamp = tx.extractTimestamp(),
|
||||
|
|
@ -2,10 +2,10 @@ package com.tangem.features.send.impl.presentation.state.recipient
|
|||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.*
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SendRecipientStateConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
|
|
@ -26,8 +26,8 @@ internal class SendRecipientStateConverter(
|
|||
memoTextField = memoFieldConverter.convertOrNull(value.memo),
|
||||
network = cryptoCurrencyStatusProvider().currency.network.name,
|
||||
isPrimaryButtonEnabled = false,
|
||||
wallets = persistentListOf(),
|
||||
recent = persistentListOf(),
|
||||
wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT),
|
||||
recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_DEFAULT_COUNT
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_KEY_TAG
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SendRecipientWalletListConverter :
|
||||
Converter<List<AvailableWallet?>, PersistentList<SendRecipientListContent>> {
|
||||
override fun convert(value: List<AvailableWallet?>): PersistentList<SendRecipientListContent> {
|
||||
return value.filterWallets().ifEmpty {
|
||||
emptyListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<AvailableWallet?>.filterWallets() = this.filterNotNull()
|
||||
.groupBy { item -> item.name }
|
||||
.values.map {
|
||||
it.mapIndexed { index, item ->
|
||||
val name = if (it.size > 1) {
|
||||
"${item.name} ${index.inc()}"
|
||||
} else {
|
||||
item.name
|
||||
}
|
||||
SendRecipientListContent(
|
||||
id = "${WALLET_KEY_TAG}$index",
|
||||
title = TextReference.Str(item.address),
|
||||
subtitle = TextReference.Str(name),
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
.toPersistentList()
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient.utils
|
||||
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal const val WALLET_DEFAULT_COUNT = 1
|
||||
internal const val RECENT_DEFAULT_COUNT = 3
|
||||
internal const val WALLET_KEY_TAG = "wallet"
|
||||
internal const val RECENT_KEY_TAG = "recent"
|
||||
|
||||
internal fun loadingListState(tag: String, count: Int) = buildList {
|
||||
repeat(count) {
|
||||
add(
|
||||
SendRecipientListContent(
|
||||
id = "$tag$it",
|
||||
isLoading = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
internal fun emptyListState(tag: String, count: Int) = buildList {
|
||||
repeat(count) {
|
||||
add(
|
||||
SendRecipientListContent(
|
||||
id = "$tag$it",
|
||||
isLoading = false,
|
||||
isVisible = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.ui
|
|||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButton
|
||||
|
|
@ -18,6 +19,11 @@ internal fun SendEventEffect(event: StateEvent<SendEvent>, snackbarHostState: Sn
|
|||
val resources = LocalContext.current.resources
|
||||
var alertConfig by remember { mutableStateOf<SendAlertState?>(value = null) }
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(key1 = alertConfig) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
|
||||
alertConfig?.let {
|
||||
SendAlert(state = it, onDismiss = { alertConfig = null })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ private fun SendingText(uiState: SendUiState, isVisible: Boolean, modifier: Modi
|
|||
val feeState = uiState.feeState
|
||||
val fiatRate = feeState?.rate
|
||||
val fiatAmount = amountState?.amountTextField?.fiatAmount
|
||||
val feeFiat = feeState?.fee?.amount?.value?.multiply(fiatRate)
|
||||
val feeFiat = fiatRate?.let { feeState.fee?.amount?.value?.multiply(it) }
|
||||
val sendingFiat = feeFiat?.let { fiatAmount?.value?.plus(it) }
|
||||
|
||||
if (feeFiat != null && sendingFiat != null) {
|
||||
|
|
@ -185,7 +185,7 @@ private fun SendDoneButtons(
|
|||
exit = slideOutVertically().plus(fadeOut()),
|
||||
label = "Animate show sent state buttons",
|
||||
) {
|
||||
Row(modifier = Modifier.padding(TangemTheme.dimens.spacing12)) {
|
||||
Row(modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12)) {
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResource(id = R.string.common_explore),
|
||||
iconResId = R.drawable.ic_web_24,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import androidx.compose.material3.SnackbarHostState
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -25,40 +24,38 @@ import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent
|
|||
import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent
|
||||
import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent
|
||||
import com.tangem.features.send.impl.presentation.ui.send.SendContent
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@Composable
|
||||
internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow<SendUiCurrentScreen>) {
|
||||
val currentState = currentStateFlow.collectAsStateWithLifecycle()
|
||||
internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val sendState = uiState.sendState ?: return
|
||||
BackHandler { uiState.clickIntents.onBackClick() }
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.imePadding()
|
||||
.systemBarsPadding()
|
||||
.background(color = TangemTheme.colors.background.tertiary),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val titleRes = when (currentState.value.type) {
|
||||
val titleRes = when (currentState.type) {
|
||||
SendUiStateType.Amount -> resourceReference(R.string.send_amount_label)
|
||||
SendUiStateType.Recipient -> resourceReference(R.string.send_recipient_label)
|
||||
SendUiStateType.Fee -> resourceReference(R.string.common_fee_selector_title)
|
||||
SendUiStateType.Send -> if (!sendState.isSuccess) {
|
||||
resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencySymbol))
|
||||
resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencyName))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
val isSending = currentState.value.type == SendUiStateType.Send && !uiState.sendState.isSuccess
|
||||
val isSending = currentState.type == SendUiStateType.Send && !uiState.sendState.isSuccess
|
||||
val subtitleRes = if (isSending) {
|
||||
uiState.amountState?.walletName
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val iconRes = if (currentState.value.type == SendUiStateType.Recipient) {
|
||||
val iconRes = if (currentState.type == SendUiStateType.Recipient) {
|
||||
R.drawable.ic_qrcode_scan_24
|
||||
} else {
|
||||
null
|
||||
|
|
@ -74,17 +71,15 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow<SendUi
|
|||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
SendScreenContent(
|
||||
uiState = uiState,
|
||||
currentState = currentState.value,
|
||||
)
|
||||
SendNavigationButtons(
|
||||
uiState = uiState,
|
||||
currentState = currentState.value,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
SendScreenContent(
|
||||
uiState = uiState,
|
||||
currentState = currentState,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
SendNavigationButtons(
|
||||
uiState = uiState,
|
||||
currentState = currentState,
|
||||
)
|
||||
}
|
||||
|
||||
SendEventEffect(
|
||||
|
|
@ -103,32 +98,36 @@ private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentS
|
|||
AnimatedContentTransitionScope.SlideDirection.End
|
||||
}
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = currentState,
|
||||
label = "Send Scree Navigation",
|
||||
modifier = modifier,
|
||||
transitionSpec = {
|
||||
lastState = currentState.type.ordinal
|
||||
slideIntoContainer(towards = direction, animationSpec = tween())
|
||||
.togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween()))
|
||||
},
|
||||
) { state ->
|
||||
when (state.type) {
|
||||
SendUiStateType.Amount -> SendAmountContent(
|
||||
amountState = uiState.amountState,
|
||||
isBalanceHiding = uiState.isBalanceHidden,
|
||||
clickIntents = uiState.clickIntents,
|
||||
)
|
||||
SendUiStateType.Recipient -> SendRecipientContent(
|
||||
uiState = uiState.recipientState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
)
|
||||
SendUiStateType.Fee -> SendSpeedAndFeeContent(
|
||||
state = uiState.feeState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
)
|
||||
SendUiStateType.Send -> SendContent(uiState)
|
||||
else -> Unit
|
||||
// Box is needed to fix animation with resizing of AnimatedContent
|
||||
Box(modifier = modifier) {
|
||||
AnimatedContent(
|
||||
targetState = currentState,
|
||||
label = "Send Scree Navigation",
|
||||
transitionSpec = {
|
||||
lastState = currentState.type.ordinal
|
||||
slideIntoContainer(towards = direction, animationSpec = tween())
|
||||
.togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween()))
|
||||
},
|
||||
) { state ->
|
||||
|
||||
when (state.type) {
|
||||
SendUiStateType.Amount -> SendAmountContent(
|
||||
amountState = uiState.amountState,
|
||||
isBalanceHiding = uiState.isBalanceHidden,
|
||||
clickIntents = uiState.clickIntents,
|
||||
)
|
||||
SendUiStateType.Recipient -> SendRecipientContent(
|
||||
uiState = uiState.recipientState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
isBalanceHidden = uiState.isBalanceHidden,
|
||||
)
|
||||
SendUiStateType.Fee -> SendSpeedAndFeeContent(
|
||||
state = uiState.feeState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
)
|
||||
SendUiStateType.Send -> SendContent(uiState)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,7 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeightIn
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment.Companion.BottomCenter
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -24,6 +22,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.job
|
||||
|
||||
@Composable
|
||||
|
|
@ -37,6 +36,16 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
|
|||
sendField.cryptoAmount to sendField.value
|
||||
}
|
||||
val requester = remember { FocusRequester() }
|
||||
var isEnabledProxy by remember { mutableStateOf(isEnabled) }
|
||||
|
||||
// Fix animation from amount screen to summary screen ([REDACTED_TASK_KEY])
|
||||
LaunchedEffect(key1 = isEnabled) {
|
||||
if (isEnabled) {
|
||||
delay(timeMillis = 700)
|
||||
}
|
||||
isEnabledProxy = isEnabled
|
||||
}
|
||||
|
||||
AmountTextField(
|
||||
value = primaryValue,
|
||||
decimals = primaryAmount.decimals,
|
||||
|
|
@ -53,7 +62,7 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
|
|||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
isEnabled = isEnabled,
|
||||
isEnabled = isEnabledProxy,
|
||||
isAutoResize = true,
|
||||
modifier = Modifier
|
||||
.focusRequester(requester)
|
||||
|
|
@ -64,6 +73,7 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
|
|||
)
|
||||
.requiredHeightIn(min = TangemTheme.dimens.size32),
|
||||
)
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
this.coroutineContext.job.invokeOnCompletion {
|
||||
requester.requestFocus()
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ internal fun SendAmountContent(
|
|||
if (amountState == null) return
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
amountField(amountState = amountState, isBalanceHiding = isBalanceHiding)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue