diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 7dda361ec6..f668224cad 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -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)) } } } diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index b76b496922..2109f0bb6b 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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 @@ -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), + ) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index d27bc8388c..6b870386be 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -46,7 +46,7 @@ class DialogManager : StoreSubscriber { 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( diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 6a963e544f..c881cd0127 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -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) { - scanResponse ?: return - val card = scanResponse.card params[AnalyticsParam.BATCH] = card.batchId params[AnalyticsParam.PRODUCT_TYPE] = getProductType(scanResponse) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index e3b376b612..a9c23d448f 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -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) : GlobalAction() + data class ChooseBehavior( + val result: CompletionResult, + val analyticsSource: AnalyticsParam.ScreensSources, + ) : GlobalAction() + object Reset : GlobalAction() object Increment : GlobalAction() } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index 79d6ed3a69..921898e383 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -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, + 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) diff --git a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt index 70ad852f71..d59f668687 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt @@ -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) { _, _ -> } diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index eb110a4d60..7a386bdfd1 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index f0f6760cfe..be433d70bb 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt index 06df7c2274..d4b5ac160c 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index cf1c9f95be..ce649ece46 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -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 -> diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index 3a0902520a..ed777bd666 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index 160d433012..99418a7aa0 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -81,6 +81,10 @@ internal class BiometricUserWalletsListManager( state.update { State() } } + override fun isLockable(): Boolean { + return true + } + override suspend fun select(userWalletId: UserWalletId): CompletionResult = catching { if (state.value.selectedUserWalletId == userWalletId) { return@catching findSelectedUserWallet()!! diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt index 1e01c26cd2..762ba82f54 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt index eedc68771b..31ef3366dd 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt @@ -85,6 +85,10 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager { state.value.userWallet ?: walletNotFound() } + override fun isLockable(): Boolean { + return false + } + private fun saveInternal(userWallet: UserWallet): CompletionResult = catching { state.update { prevState -> prevState.copy( diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index ab68f8b273..39dc386ee6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 8453733f19..1819ad46c5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index cb2d157825..2266971dad 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 5851a4f01c..2048292209 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -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)) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index f17e57ac79..33784bac12 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt index 936a202358..5e0028b00d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index deb5bb4873..5c7b1e8589 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -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())) }, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt index ddec9cdfb3..ace9edcf19 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt index 9cf4359f72..cf27394348 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt index f862ff5e94..5687caa590 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt @@ -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) { _, _ -> } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt index d5328878eb..e122327ff8 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt @@ -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 + } + }, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 916d89105a..b59a973b64 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -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) } }, diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 3782449fe2..32c48ed96d 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -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?, diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 5f53ab40d7..a20223d2cb 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -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) { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index 8820f8ff12..9f39ae5bdd 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -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, + ), + ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index ba04ed3f0f..32ea540a36 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -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), ) } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 417d8f8b73..4071534469 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -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) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt index 2b1de6b7b0..905fe4f767 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt @@ -72,6 +72,12 @@ class AppPreferencesStore( return this[key]?.let(adapter::fromJson).orEmpty() } + /** Get set of data [T] by string [key] */ + inline fun MutablePreferences.getObjectSet(key: Preferences.Key): Set? { + val adapter = moshi.adapter>(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 MutablePreferences.setObjectSet(key: Preferences.Key, value: Set) { + val adapter = moshi.adapter>(Types.newParameterizedType(Set::class.java, T::class.java)) + this[key] = adapter.toJson(value) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 364a467ff3..4dfd9d6966 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -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") } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index cd1facee55..aa3171803c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -128,6 +128,15 @@ suspend inline fun AppPreferencesStore.getObjectMap(key: Preferences val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) val adapter = moshi.adapter>(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 AppPreferencesStore.getObjectSetSync(key: Preferences.Key): Set { + val adapter = moshi.adapter>(Types.newParameterizedType(Set::class.java, T::class.java)) return data.firstOrNull() ?.get(key) ?.let(adapter::fromJson) diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 5598afe138..d5cd06e891 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -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", diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt b/core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt index 416307b644..75aa1f0579 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt @@ -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; + } } \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 136cc2857f..1e87046df0 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -79,7 +79,7 @@ Посмотреть историю транзакций Обозреватель Комиссия - Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s + Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s Свое Быстро По рынку @@ -310,7 +310,7 @@ В этом случае вам будет необходимо начать процесс заново. Вы хотите выйти из процесса активации? Подготовка - Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа? + Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Если на нем есть средства, пожалуйста сначала выведите их, а затем сделайте сброс до заводских настроек и используйте как резервную. Резервная копия Прочитать о seed-фразе @@ -426,12 +426,12 @@ Приготовьте свою карту Уже содержится в введенном адресе Вычесть - Недостаточно средств для покрытия комиссии сети. Вычесть комиссию %s из отправляемой сумму? + Недостаточно средств для покрытия комиссии сети. Вычесть недостающую сумму для покрытия комиссии из отправляемой суммы? + Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? Причина: %1$s\nКод: %2$s Транзакция не выполнена Сумма - Подтверждение %1$s, %2$s Адрес Код назначения @@ -487,6 +487,7 @@ Отправить Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. Мои кошельки + Это способ измерения комиссии за отправку биткоин-транзакции. Он указывает на количество самой маленькой единицы биткоина (сатоши) за каждый байт данных в транзакции. Чем выше это число, тем быстрее будет обработана транзакция сетью. Отправка Нажмите на любое поле, чтобы изменить его Отправка %s @@ -536,7 +537,8 @@ Балансы скрыты Балансы показаны Отменить - В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением.” + Выбранная операция в данный момент недоступна. Попробуйте позже. + В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением. У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. Выбранная операция в данный момент недоступна. Попробуйте позже. Обмен %s не доступен. Но мы работаем над его добавлением. @@ -547,6 +549,7 @@ Скрыть %s Скрыть токен %1$s токен в сети %%image%% %2$s + Токен в сети %%image%% %1$s Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. Невозможно скрыть %s Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index eacae3a8ac..f0b93b187c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -310,7 +310,7 @@ In this case, you will need to start from the beginning. Do you want to exit the activation process? Getting started - 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? + 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. Creating a backup Read more about seed phrase @@ -421,12 +421,12 @@ Get your card ready! Already included in the entered address Subtract - Not enough funds to cover the network commission. Subtract the commission %s from the amount sent? + Not enough funds to cover the network fee. Do you want to subtract the amount required to cover the fee? + The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue? Reason: %1$s\nCode: %2$s The transaction is not completed Amount - Confirm %1$s, %2$s Address Destination Tag @@ -482,6 +482,8 @@ Send to 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 My wallets + 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. + Satoshi per vbyte Sending... Tap any field to change it Send %s @@ -532,6 +534,7 @@ Balances hidden Balances shown Undo + This operation is currently unavailable. Please try again later. The purchase of the %s is currently unavailable. But we are working on adding it. You do not have funds to send. Top up your account to be able to send funds from it. This operation is currently unavailable. Please try again later. @@ -543,6 +546,7 @@ Hide %s Hide token %1$s token in %%image%% %2$s network + Token in %%image%% %1$s network 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. Unable to hide %s Exchange this token for another at %1$s service fees from February %2$s-%3$s. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index 6d31ebeaef..b9cba56550 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -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), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt index 5ef8a2c4e3..8f10352030 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt @@ -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 { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt index 00d4221e37..f3d8a0c1cc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt @@ -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, diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 3ca0b57adf..42309223b1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -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 -> diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt index 8cc2cf7bda..5006df1d7a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt @@ -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) } \ No newline at end of file diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt index 87729e8bc4..cce12822c5 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt @@ -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)) } diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt index ad1f6244a7..3b3cd6df38 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt @@ -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) { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 05c56cfaf9..062692cd06 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index b3092b4f38..7f1ae0f462 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -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, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultPolkadotAccountHealthCheckRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultPolkadotAccountHealthCheckRepository.kt new file mode 100644 index 0000000000..049991280b --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultPolkadotAccountHealthCheckRepository.kt @@ -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>() + private val hasResetTransaction = MutableSharedFlow>() + + private val mutex = Mutex() + private val mutexes = ConcurrentHashMap() + + 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(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(POLKADOT_HEALTH_CHECKED_IMMUTABLE_ACCOUNTS_KEY) + if (checkedAddresses.contains(address)) return + + runCatching { + do { + // Getting batch of extrinsics to check + val lastChecked = appPreferencesStore.getObjectMap(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(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(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) { + appPreferencesStore.editData { mutablePreferences -> + val savedList = mutablePreferences.getObjectSet(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" + } +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index 9f4801ff8c..eada356d1e 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -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 { - 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 { diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt index 4dfc1c9668..ff96eda7f5 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt @@ -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 suspend fun scan( - analyticsEvent: AnalyticsEvent? = null, + analyticsSource: AnalyticsParam.ScreensSources, cardId: String? = null, onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, onWalletNotCreated: suspend () -> Unit = {}, diff --git a/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt b/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt index 304c70af48..7b9bb1a996 100644 --- a/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt +++ b/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt @@ -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") diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 6e18152f25..eded0146fc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -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, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasImmortalUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasImmortalUseCase.kt new file mode 100644 index 0000000000..527f6e1703 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasImmortalUseCase.kt @@ -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> = + polkadotAccountHealthCheckRepository.subscribeToHasImmortalResults() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasResetUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasResetUseCase.kt new file mode 100644 index 0000000000..33ce7e960e --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPolkadotCheckHasResetUseCase.kt @@ -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> = + polkadotAccountHealthCheckRepository.subscribeToHasResetResults() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RunPolkadotAccountHealthCheckUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RunPolkadotAccountHealthCheckUseCase.kt new file mode 100644 index 0000000000..f7b3131361 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RunPolkadotAccountHealthCheckUseCase.kt @@ -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 = Either.catch { + polkadotAccountHealthCheckRepository.runCheck(userWalletId, network) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PolkadotAccountHealthCheckRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PolkadotAccountHealthCheckRepository.kt new file mode 100644 index 0000000000..a0cd15dd47 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PolkadotAccountHealthCheckRepository.kt @@ -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> + + fun subscribeToHasResetResults(): Flow> +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt index 753f45e4b4..22b7991290 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -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() diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 059a672c70..d2c9cbd085 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -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 -> { diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt index b010a54db4..652b15c85b 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt @@ -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, diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt index 974d439c7e..cedb309901 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt @@ -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 { return either { catch( @@ -22,4 +24,17 @@ class GetExplorerTransactionUrlUseCase( ) } } + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either { + return either { + catch( + block = { + repository.getTxExploreUrl(userWalletId, network).ifEmpty { + raise(TxStatusError.EmptyUrlError) + } + }, + catch = { raise(TxStatusError.DataError(it)) }, + ) + } + } } \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt index aa93bec246..8b5a463618 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt @@ -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> { + return Either.catch { + repository.getFixedSizeTxHistoryItems(userWalletId, currency, pageSize, refresh) + }.mapLeft { TxHistoryListError.DataError(it) } + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt index 8a63233bed..213d780d07 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt @@ -84,6 +84,11 @@ interface UserWalletsListManager { */ suspend fun get(userWalletId: UserWalletId): CompletionResult + /** + * Indicates that the [UserWalletsListManager] supports [UserWalletsListManager.Lockable] + * */ + fun isLockable(): Boolean + interface Lockable : UserWalletsListManager { /** diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt index 0003a1300d..a6bdbde07f 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt @@ -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 } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt index 6edcbe2ea2..31f79da70e 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt @@ -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), ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 0a3521b1eb..64e8f1e992 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -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) } } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt index 0837f1f27c..03e9414c0a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendRecipientListContent.kt @@ -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, ) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt index 93bb812fae..6de841373c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendAlertState.kt @@ -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) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt index 2928e3d1f6..d99b6bd0ff 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendEventStateFactory.kt @@ -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, + private val cryptoCurrencyStatusProvider: Provider, 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, + ), + ) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index f50a061117..f3f304b7ff 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -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, private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, - private val feeCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, private val isTapHelpPreviewEnabledProvider: Provider, 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, txHistory: List): SendUiState = - recipientListStateConverter.convert( - wallets = wallets, - txHistory = txHistory, + fun onLoadedWalletsList(wallets: List): SendUiState { + val state = currentStateProvider() + return state.copy( + recipientState = state.recipientState?.copy( + wallets = recipientWalletListStateConverter.convert(wallets), + ), ) + } + + fun onLoadedHistoryList(txHistory: List): 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(), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt index a35b421a16..7f6a8588b1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendTransactionAlertConverter.kt @@ -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, private val clickIntents: SendClickIntents, ) : Converter { 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 } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index a4306da4f4..586c55c978 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -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, 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, ) : SendStates() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountNotificationFactory.kt deleted file mode 100644 index 222a8704cf..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountNotificationFactory.kt +++ /dev/null @@ -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, - private val currentStateProvider: Provider, - private val clickIntents: SendClickIntents, -) { - - fun create() = stateRouterProvider().currentState - .filter { it.type == SendUiStateType.Amount } - .map { - buildList { - addFeeUnreachableNotification() - }.toImmutableList() - } - - private fun MutableList.addFeeUnreachableNotification() { - val state = currentStateProvider() - val feeState = state.feeState ?: return - - if (feeState.feeSelectorState is FeeSelectorState.Error) { - add( - SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload), - ) - } - } -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt index d12d7d3c28..f06a20bf48 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt @@ -41,6 +41,7 @@ internal class SendAmountStateConverter( notifications = persistentListOf(), isFeeLoading = false, appCurrencyCode = appCurrency.code, + subtractedFee = null, segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) { persistentListOf() } else { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt index bcad6f7468..bf0f52dd73 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt @@ -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, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index fa2c3a3ba7..e71cdf4d14 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -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, private val coinCryptoCurrencyStatusProvider: Provider, + private val feePaidCryptoCurrencyStatusProvider: Provider, private val currentStateProvider: Provider, private val userWalletProvider: Provider, 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.addInvalidAmountNotification( - isSubtractAmount: Boolean, - receivedAmount: BigDecimal, - ) { - if (isSubtractAmount && receivedAmount <= BigDecimal.ZERO) { - add(SendNotification.Error.InvalidAmount) - } - } - private fun MutableList.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.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.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.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") } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt index 1a57669334..57e567a586 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt @@ -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 } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt index bd8e832a91..1fa99ee291 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt @@ -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, - private val feeCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter { - 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( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt index 217631c38e..92189a24ef 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt @@ -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.addTooHighNotification( - transactionFee: TransactionFee, - selectedFee: FeeType, - customFee: List, - ) { - 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 - } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index 997c39d2f1..ba181f423f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -21,7 +21,7 @@ import kotlinx.collections.immutable.persistentListOf internal class FeeStateFactory( private val clickIntents: SendClickIntents, private val currentStateProvider: Provider, - private val feeCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, 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, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt index cdd0fa60e6..c9534ea97f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -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, - private val feeCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter> { - 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 { 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 }, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index caf0ee5683..d9b92b387d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -9,7 +9,7 @@ import kotlinx.collections.immutable.persistentListOf internal class SendFeeStateConverter( private val appCurrencyProvider: Provider, - private val feeCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter { 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, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt new file mode 100644 index 0000000000..8226a67504 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt @@ -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, + private val feeCryptoCurrencyStatusProvider: Provider, +) : CustomFeeConverter { + + override fun convert(value: Fee.Bitcoin): ImmutableList { + 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): 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, + index: Int, + value: String, + txSize: BigDecimal, + ): ImmutableList { + 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 + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/CustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/CustomFeeConverter.kt new file mode 100644 index 0000000000..25764269ec --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/CustomFeeConverter.kt @@ -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 : Converter> { + fun convertBack(normalFee: T, value: ImmutableList): T +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index 11dfc8f056..4851533b49 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -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, - private val feeCryptoCurrencyStatusProvider: Provider, -) : Converter> { + private val feeCryptoCurrencyStatusProvider: Provider, +) : CustomFeeConverter { override fun convert(value: Fee.Ethereum): ImmutableList { 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): Fee.Ethereum { + override fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList): 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 diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index 273e457783..8d7daa1665 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -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 + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt index b29459e757..715d3ad4f8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt @@ -34,6 +34,7 @@ internal class SendAmountFieldMaxAmountConverter( return state.copy( amountState = amountState.copy( isPrimaryButtonEnabled = true, + subtractedFee = null, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index 8db8a2fc02..266f9e8f6e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -64,5 +64,6 @@ internal sealed class SendTextField { val title: TextReference, val footer: TextReference, val label: TextReference? = null, + val isReadonly: Boolean = false, ) : SendTextField() } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt index 75e5ce97f4..91dbbde9bf 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt @@ -25,6 +25,7 @@ internal object AmountStatePreviewData { notifications = persistentListOf(), isFeeLoading = false, appCurrencyCode = "usd", + subtractedFee = null, amountTextField = SendTextField.AmountField( value = "123.123123123123123123", onValueChange = {}, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt index c07c45a39b..766380d826 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt @@ -48,6 +48,7 @@ internal object FeeStatePreviewData { ), isFeeApproximate = false, notifications = persistentListOf(), + isCustomSelected = false, ) val errorFeeState = feeState.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt index d70b281ffe..92be46efa2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt @@ -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) {} + override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class) {} override fun onNotificationCancel(clazz: Class) {} } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt similarity index 71% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt rename to features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt index 046c7a153d..f6ccb6ba98 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientListConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt @@ -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, +internal class SendRecipientHistoryListConverter( private val cryptoCurrencyStatusProvider: Provider, -) { +) : Converter, ImmutableList> { - fun convert(wallets: List, txHistory: List): SendUiState { + override fun convert(value: List): ImmutableList { 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.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.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(), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt index dd7db52c45..9b3f770fcf 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt @@ -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), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt new file mode 100644 index 0000000000..855e5d3a3b --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientWalletListConverter.kt @@ -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, PersistentList> { + override fun convert(value: List): PersistentList { + return value.filterWallets().ifEmpty { + emptyListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT) + } + } + + private fun List.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() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/utils/RecentListUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/utils/RecentListUtils.kt new file mode 100644 index 0000000000..c2d5ad9739 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/utils/RecentListUtils.kt @@ -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() \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt index 84e638eb8a..53378ca7f7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendEventEffect.kt @@ -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, snackbarHostState: Sn val resources = LocalContext.current.resources var alertConfig by remember { mutableStateOf(value = null) } + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(key1 = alertConfig) { + keyboardController?.hide() + } + alertConfig?.let { SendAlert(state = it, onDismiss = { alertConfig = null }) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 5bdb10824e..fda14b8a8a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -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, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index b227fe07fe..caf0a2f4f2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -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) { - 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 - 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 + } } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index 5aa3ec82ac..bb5b97aa51 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -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() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index 2156dc6f48..0f5b6493e8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -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) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt index c4634330ae..fcff795b70 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt @@ -21,11 +21,6 @@ internal fun LazyListScope.notifications( key = { _, item -> item::class.java }, contentType = { _, item -> item::class.java }, itemContent = { i, item -> - val bottomPadding = if (i == notifications.lastIndex) { - TangemTheme.dimens.spacing72 - } else { - TangemTheme.dimens.spacing0 - } val topPadding = if (i == 0 && hasPaddingAbove) { TangemTheme.dimens.spacing0 } else { @@ -34,10 +29,7 @@ internal fun LazyListScope.notifications( Notification( config = item.config, modifier = modifier - .padding( - top = topPadding, - bottom = bottomPadding, - ) + .padding(top = topPadding) .animateItemPlacement(), containerColor = when (item) { is SendNotification.Error.ExceedsBalance, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt similarity index 90% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt rename to features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt index 50fde69ede..1c7e332844 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt @@ -17,7 +17,7 @@ import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import kotlinx.collections.immutable.ImmutableList @Composable -internal fun SendCustomFeeEthereum( +internal fun SendCustomFee( customValues: ImmutableList, selectedFee: FeeType, hasNotifications: Boolean, @@ -29,20 +29,19 @@ internal fun SendCustomFeeEthereum( enter = expandVertically().plus(fadeIn()), exit = shrinkVertically().plus(fadeOut()), ) { + val bottomPadding = if (hasNotifications) { + TangemTheme.dimens.spacing12 + } else { + TangemTheme.dimens.spacing0 + } Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - modifier = modifier, + modifier = modifier.padding(bottom = bottomPadding), ) { repeat(customValues.size) { index -> val value = customValues[index] - val bottomPadding = if (index == customValues.lastIndex && !hasNotifications) { - TangemTheme.dimens.spacing72 - } else { - TangemTheme.dimens.spacing0 - } FooterContainer( footer = value.footer.resolveReference(), - modifier = Modifier.padding(bottom = bottomPadding), ) { if (value.label != null) { InputRowEnterInfoAmount( @@ -55,6 +54,7 @@ internal fun SendCustomFeeEthereum( keyboardActions = value.keyboardActions, onValueChange = value.onValueChange, showDivider = false, + isReadOnly = value.isReadonly, modifier = Modifier .background( color = TangemTheme.colors.background.action, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt index ee4055bff5..88f4a37a85 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedAndFeeContent.kt @@ -31,7 +31,9 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S .fillMaxSize() .background(TangemTheme.colors.background.tertiary) .padding( - horizontal = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, ), ) { feeSelector(state, clickIntents) @@ -64,7 +66,7 @@ internal fun LazyListScope.customFee( item( key = FEE_CUSTOM_KEY, ) { - SendCustomFeeEthereum( + SendCustomFee( customValues = feeSendState.customValues, selectedFee = feeSendState.selectedFee, hasNotifications = hasNotifications, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index 3ecc6ddf2e..0a2a65c93e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -3,7 +3,6 @@ package com.tangem.features.send.impl.presentation.ui.fee import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.* -import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -12,23 +11,17 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.state.SendNotification import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType -import java.math.BigDecimal +import com.tangem.features.send.impl.presentation.utils.getCryptoReference +import com.tangem.features.send.impl.presentation.utils.getFiatReference @Composable internal fun SendSpeedSelectorItem( @@ -67,42 +60,10 @@ internal fun SendSpeedSelectorItem( showDivider = showDivider, ) SendSpeedSelectorItemError(isError = feeSelectorState is FeeSelectorState.Error) - - if (feeType == FeeType.Custom) { - val showWarning = state.notifications.any { it is SendNotification.Warning.TooHigh } - WarningIcon(showWarning = showWarning) - } } } } -// todo remove after refactoring [REDACTED_JIRA] -private fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { - if (amount == null) return null - return combinedReference( - if (isFeeApproximate) stringReference("${BigDecimalFormatter.CAN_BE_LOWER_SIGN} ") else TextReference.EMPTY, - stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amount.value, - cryptoCurrency = amount.currencySymbol, - decimals = amount.decimals, - ), - ), - ) -} - -// todo remove after refactoring [REDACTED_JIRA] -private fun getFiatReference(amount: Amount?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { - if (amount == null) return null - return stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = rate?.let { amount.value?.multiply(it) }, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), - ) -} - @Composable private fun SendSpeedSelectorItemError(isError: Boolean) { Row { @@ -127,29 +88,6 @@ private fun SendSpeedSelectorItemError(isError: Boolean) { } } -@Composable -private fun WarningIcon(showWarning: Boolean = false) { - Row { - SpacerWMax() - AnimatedVisibility( - visible = showWarning, - label = "Custom fee warning indicator", - enter = fadeIn(), - exit = fadeOut(), - ) { - Image( - painter = painterResource(R.drawable.ic_alert_triangle_20), - contentDescription = null, - modifier = Modifier - .padding( - vertical = TangemTheme.dimens.spacing12, - horizontal = TangemTheme.dimens.spacing14, - ), - ) - } - } -} - private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? { val choosableFees = fees as? TransactionFee.Choosable return when (feeType) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt index c33885f290..9ef9f60a21 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt @@ -1,6 +1,10 @@ package com.tangem.features.send.impl.presentation.ui.recipient import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -9,6 +13,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource @@ -16,6 +21,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.icons.identicon.IdentIcon @@ -43,9 +50,42 @@ fun ListItemWithIcon( info: String? = null, subtitleEndOffset: Int = 0, @DrawableRes subtitleIconRes: Int? = null, + isLoading: Boolean = false, +) { + AnimatedContent( + targetState = isLoading, + label = "Recent List Content Animation", + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + ) { isLoadingState -> + if (isLoadingState) { + ListItemLoading(modifier = modifier) + } else { + ListItemWithIcon( + title = title, + subtitle = subtitle, + onClick = onClick, + info = info, + subtitleEndOffset = subtitleEndOffset, + subtitleIconRes = subtitleIconRes, + modifier = modifier, + ) + } + } +} + +@Composable +private fun ListItemWithIcon( + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + info: String? = null, + subtitleEndOffset: Int = 0, + @DrawableRes subtitleIconRes: Int? = null, ) { val hapticFeedback = rememberHapticFeedback(state = title, onAction = onClick) Row( + verticalAlignment = Alignment.CenterVertically, modifier = modifier .fillMaxWidth() .clickable { hapticFeedback() } @@ -55,13 +95,12 @@ fun ListItemWithIcon( address = title, modifier = Modifier .padding(vertical = TangemTheme.dimens.spacing8) - .size(TangemTheme.dimens.size40) - .clip(RoundedCornerShape(TangemTheme.dimens.radius20)), + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius18)), ) Column( - modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing10) - .padding(start = TangemTheme.dimens.spacing12), + modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.SpaceBetween, ) { EllipsisText( text = title, @@ -102,6 +141,43 @@ fun ListItemWithIcon( } } +@Composable +private fun ListItemLoading(modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing12), + ) { + CircleShimmer( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing8) + .size(TangemTheme.dimens.size36), + ) + Column( + modifier = Modifier + .height(TangemTheme.dimens.size32) + .padding(start = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.SpaceBetween, + ) { + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier.size( + width = TangemTheme.dimens.spacing70, + height = TangemTheme.dimens.spacing12, + ), + ) + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier.size( + width = TangemTheme.dimens.spacing52, + height = TangemTheme.dimens.spacing12, + ), + ) + } + } +} + // region preview @Preview @Composable @@ -115,6 +191,7 @@ private fun ListItemWithIconPreview_Light( subtitleEndOffset = config.subtitleEndOffset, subtitleIconRes = config.iconRes, onClick = {}, + isLoading = config.isLoading, ) } } @@ -131,6 +208,7 @@ private fun ListItemWithIconPreview_Dark( subtitleEndOffset = config.subtitleEndOffset, subtitleIconRes = config.iconRes, onClick = {}, + isLoading = config.isLoading, ) } } @@ -141,6 +219,7 @@ private data class ListItemWithIconPreviewConfig( val info: String? = null, val subtitleEndOffset: Int = 0, val iconRes: Int? = null, + val isLoading: Boolean = false, ) private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvider( @@ -163,6 +242,14 @@ private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvid title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", subtitle = "Wallet", ), + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "0.000000000000000000000000000000 BTC", + info = "0.0.0000 at 00:00", + subtitleEndOffset = "BTC".length, + iconRes = R.drawable.ic_arrow_down_24, + isLoading = true, + ), ), ) //endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 93910bc2a6..f728ff97cb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -2,8 +2,8 @@ package com.tangem.features.send.impl.presentation.ui.recipient import androidx.annotation.StringRes import androidx.compose.animation.* -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -18,6 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.inputrow.InputRowRecipient import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -25,6 +26,7 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList @@ -33,7 +35,11 @@ private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY" @Composable -internal fun SendRecipientContent(uiState: SendStates.RecipientState?, clickIntents: SendClickIntents) { +internal fun SendRecipientContent( + uiState: SendStates.RecipientState?, + clickIntents: SendClickIntents, + isBalanceHidden: Boolean, +) { if (uiState == null) return val recipients = uiState.recent val wallets = uiState.wallets @@ -45,75 +51,101 @@ internal fun SendRecipientContent(uiState: SendStates.RecipientState?, clickInte modifier = Modifier .fillMaxSize() .background(TangemTheme.colors.background.tertiary) - .padding(horizontal = TangemTheme.dimens.spacing16), + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), ) { - item(key = ADDRESS_FIELD_KEY) { - FooterContainer( - footer = stringResource(R.string.send_recipient_address_footer, uiState.network), - ) { - InputRowRecipient( - value = address.value, - title = address.label, - placeholder = address.placeholder, - onValueChange = address.onValueChange, - onPasteClick = { clickIntents.onRecipientAddressValueChange(it, EnterAddressSource.PasteButton) }, - isError = isError, - isLoading = isValidating, - error = address.error, - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) - } - } - if (memoField != null) { - item(key = MEMO_FIELD_KEY) { - val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText - TextFieldWithPaste( - value = memoField.value, - label = memoField.label, - placeholder = placeholder, - footer = stringResource(R.string.send_recipient_memo_footer), - onValueChange = memoField.onValueChange, - onPasteClick = clickIntents::onRecipientMemoValueChange, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), - isError = memoField.isError, - error = memoField.error, - isReadOnly = !memoField.isEnabled, - ) - } - } + addressItem( + address = address, + network = uiState.network, + isError = isError, + isValidating = isValidating, + onAddressChange = clickIntents::onRecipientAddressValueChange, + ) + memoField( + memoField = memoField, + onMemoChange = clickIntents::onRecipientMemoValueChange, + ) listHeaderItem( titleRes = R.string.send_recipient_wallets_title, isVisible = wallets.isNotEmpty() && wallets.first().isVisible, isFirst = true, ) - listItem(wallets, clickIntents, isLast = recipients.isEmpty()) + listItem( + list = wallets, + clickIntents = clickIntents, + isLast = recipients.any { !it.isVisible }, + isBalanceHidden = isBalanceHidden, + ) listHeaderItem( titleRes = R.string.send_recent_transactions, isVisible = recipients.isNotEmpty() && recipients.first().isVisible, - isFirst = wallets.isEmpty(), + isFirst = wallets.any { !it.isVisible }, + ) + listItem( + list = recipients, + clickIntents = clickIntents, + isLast = true, + isBalanceHidden = isBalanceHidden, ) - listItem(recipients, clickIntents, isLast = true) } } -@OptIn(ExperimentalFoundationApi::class) -private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Boolean, isFirst: Boolean) { - item( - key = titleRes, - ) { - AnimatedVisibility( - visible = isVisible, - label = "Header Appearance Animation", - enter = slideInVertically() + fadeIn(), - exit = slideOutVertically() + fadeOut(), - modifier = Modifier - .animateItemPlacement() - .animateContentSize(), +private fun LazyListScope.addressItem( + address: SendTextField.RecipientAddress, + network: String, + isError: Boolean, + isValidating: Boolean, + onAddressChange: (String, EnterAddressSource?) -> Unit, +) { + item(key = ADDRESS_FIELD_KEY) { + FooterContainer( + footer = stringResource(R.string.send_recipient_address_footer, network), ) { + InputRowRecipient( + value = address.value, + title = address.label, + placeholder = address.placeholder, + onValueChange = address.onValueChange, + onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) }, + isError = isError, + isLoading = isValidating, + error = address.error, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + } +} + +private fun LazyListScope.memoField(memoField: SendTextField.RecipientMemo?, onMemoChange: (String) -> Unit) { + if (memoField != null) { + item(key = MEMO_FIELD_KEY) { + val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText + TextFieldWithPaste( + value = memoField.value, + label = memoField.label, + placeholder = placeholder, + footer = stringResource(R.string.send_recipient_memo_footer), + onValueChange = memoField.onValueChange, + onPasteClick = onMemoChange, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), + isError = memoField.isError, + error = memoField.error, + isReadOnly = !memoField.isEnabled, + ) + } + } +} + +private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Boolean, isFirst: Boolean) { + item(key = titleRes) { + AnimateRecentAppearance(isVisible) { val (topPadding, paddingFromTop) = if (isFirst) { TangemTheme.dimens.spacing20 to TangemTheme.dimens.spacing12 } else { @@ -149,11 +181,11 @@ private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Bo } } -@OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.listItem( list: ImmutableList, clickIntents: SendClickIntents, isLast: Boolean, + isBalanceHidden: Boolean, ) { items( count = list.size, @@ -162,27 +194,24 @@ private fun LazyListScope.listItem( ) { index -> val item = list[index] val title = item.title.resolveReference() - AnimatedVisibility( - visible = item.isVisible, - label = "Header Appearance Animation", - enter = slideInVertically() + fadeIn(), - exit = slideOutVertically() + fadeOut(), - modifier = Modifier - .animateItemPlacement() - .animateContentSize(), - ) { + AnimateRecentAppearance(item.isVisible) { ListItemWithIcon( title = title, - subtitle = item.subtitle.resolveReference(), + subtitle = if (isBalanceHidden) STARS else item.subtitle.resolveReference(), info = item.timestamp?.resolveReference(), subtitleEndOffset = item.subtitleEndOffset, subtitleIconRes = item.subtitleIconRes, - onClick = { clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress) }, + onClick = { + clickIntents.onRecipientAddressValueChange( + title, + EnterAddressSource.RecentAddress, + ) + }, + isLoading = item.isLoading, modifier = Modifier .then( if (isLast && index == list.lastIndex) { Modifier - .padding(bottom = TangemTheme.dimens.spacing72) .clip( shape = RoundedCornerShape( bottomStart = TangemTheme.dimens.radius16, @@ -197,4 +226,22 @@ private fun LazyListScope.listItem( ) } } +} + +@Composable +private fun AnimateRecentAppearance(isVisible: Boolean, content: @Composable () -> Unit) { + AnimatedContent( + targetState = isVisible, + label = "Item Appearance Animation", + transitionSpec = { + (slideInHorizontally() + fadeIn()) + .togetherWith(slideOutVertically() + fadeOut()) + }, + ) { + if (it) { + content() + } else { + Box(modifier = Modifier.fillMaxWidth()) + } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index 6109c5ff50..5c0ca33a84 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -1,5 +1,8 @@ package com.tangem.features.send.impl.presentation.ui.recipient +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.* import androidx.compose.material3.Text @@ -63,17 +66,29 @@ internal fun TextFieldWithPaste( .padding(top = TangemTheme.dimens.spacing6), ) } - CrossIcon( - onClick = onPasteClick, + AnimatedVisibility( + visible = !isReadOnly, + label = "Animate read only status change", + enter = fadeIn(), + exit = fadeOut(), modifier = Modifier .align(CenterVertically), - ) + ) { + CrossIcon( + onClick = onPasteClick, + ) + } } - if (!isReadOnly) { + AnimatedVisibility( + visible = !isReadOnly, + label = "Animate read only status change", + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier.align(CenterEnd), + ) { PasteButton( isPasteButtonVisible = value.isBlank(), onClick = onPasteClick, - modifier = Modifier.align(CenterEnd), ) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt index 528cdfea00..85ff055ed6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt @@ -30,13 +30,9 @@ internal fun AmountBlock( ) { val amount = amountState.amountTextField - val cryptoAmount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amount.cryptoAmount.value, - cryptoCurrency = amount.cryptoAmount.currencySymbol, - decimals = amount.cryptoAmount.decimals, - ) - val fiatAmount = BigDecimalFormatter.formatFiatAmount( - fiatAmount = amount.fiatAmount.value, + val cryptoAmount = BigDecimalFormatter.formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) + val fiatAmount = BigDecimalFormatter.formatFiatEditableAmount( + fiatAmount = amount.fiatValue, fiatCurrencySymbol = amount.fiatAmount.currencySymbol, fiatCurrencyCode = amountState.appCurrencyCode, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index 23af8a4893..a3ee42ccbb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -15,14 +15,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.rows.SelectorRowItem -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendStates 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.state.previewdata.FeeStatePreviewData +import com.tangem.features.send.impl.presentation.utils.getCryptoReference +import com.tangem.features.send.impl.presentation.utils.getFiatReference @Composable internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { @@ -34,16 +34,6 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24 FeeType.Custom -> R.string.common_fee_selector_option_custom to R.drawable.ic_edit_24 } - val feeCryptoValue = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = fee.amount.value, - cryptoCurrency = fee.amount.currencySymbol, - decimals = fee.amount.decimals, - ) - val feeFiatValue = BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeState.rate?.let { fee.amount.value?.multiply(it) }, - fiatCurrencyCode = feeState.appCurrency.code, - fiatCurrencySymbol = feeState.appCurrency.symbol, - ) Column( modifier = Modifier .fillMaxWidth() @@ -60,8 +50,8 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick SelectorRowItem( titleRes = title, iconRes = icon, - preDot = stringReference(feeCryptoValue), - postDot = stringReference(feeFiatValue), + preDot = getCryptoReference(fee.amount, feeState.isFeeApproximate), + postDot = getFiatReference(fee.amount, feeState.rate, feeState.appCurrency), ellipsizeOffset = fee.amount.currencySymbol.length, isSelected = true, showDivider = false, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt index 473d043d35..f276edf3c1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -41,7 +40,6 @@ internal fun RecipientBlock( .padding(TangemTheme.dimens.spacing12), ) { AddressBlock(recipientState.addressTextField) - MemoBlock(recipientState.memoTextField) } } @@ -72,28 +70,6 @@ private fun AddressBlock(address: SendTextField.RecipientAddress) { } } -@Composable -private fun MemoBlock(memo: SendTextField.RecipientMemo?) { - val showMemo = memo != null && memo.value.isNotBlank() - if (showMemo) { - HorizontalDivider( - color = TangemTheme.colors.stroke.primary, - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), - ) - Text( - text = memo?.label?.resolveReference().orEmpty(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - ) - Text( - text = memo?.value.orEmpty(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), - ) - } -} - // region Preview @Preview @Composable diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt new file mode 100644 index 0000000000..022d0c5711 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt @@ -0,0 +1,34 @@ +package com.tangem.features.send.impl.presentation.utils + +import com.tangem.blockchain.common.Amount +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import java.math.BigDecimal + +internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { + if (amount == null) return null + return combinedReference( + if (isFeeApproximate) stringReference("${BigDecimalFormatter.CAN_BE_LOWER_SIGN} ") else TextReference.EMPTY, + stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = amount.value, + cryptoCurrency = amount.currencySymbol, + decimals = amount.decimals, + ), + ), + ) +} + +internal fun getFiatReference(amount: Amount?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { + if (amount == null) return null + return stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = rate?.let { amount.value?.multiply(it) }, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index c35cf2ba2d..9db5547b20 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -5,6 +5,7 @@ import com.tangem.domain.wallets.models.UserWalletId 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 java.math.BigDecimal @Suppress("TooManyFunctions") internal interface SendClickIntents { @@ -38,7 +39,7 @@ internal interface SendClickIntents { // endregion // region Fee - fun feeReload() + fun feeReload(isToNextState: Boolean = false) fun onFeeSelectorClick(feeType: FeeType) @@ -64,7 +65,7 @@ internal interface SendClickIntents { fun onShareClick() - fun onAmountReduceClick(reducedAmount: String, clazz: Class) + fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class) fun onNotificationCancel(clazz: Class) // endregion diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 67e152dae9..ffad3f3633 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.viewmodel +import android.os.SystemClock import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -9,6 +10,7 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -29,10 +31,8 @@ import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase -import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -47,7 +47,6 @@ import com.tangem.features.send.impl.presentation.analytics.SendScreenSource import com.tangem.features.send.impl.presentation.analytics.utils.SendOnNextScreenAnalyticSender import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.* -import com.tangem.features.send.impl.presentation.state.amount.AmountNotificationFactory import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory import com.tangem.features.send.impl.presentation.state.fee.* @@ -57,10 +56,10 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber +import java.math.BigDecimal import java.util.Locale import javax.inject.Inject import kotlin.properties.Delegates @@ -75,15 +74,15 @@ internal class SendViewModel @Inject constructor( private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, - private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getCryptoCurrencyStatusesSyncUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, - private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -91,9 +90,9 @@ internal class SendViewModel @Inject constructor( private val parseQrCodeUseCase: ParseQrCodeUseCase, private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, - getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, savedStateHandle: SavedStateHandle, @@ -125,7 +124,6 @@ internal class SendViewModel @Inject constructor( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, validateWalletMemoUseCase = validateWalletMemoUseCase, - getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase, isTapHelpPreviewEnabledProvider = Provider { isTapHelpPreviewEnabled }, ) @@ -145,15 +143,10 @@ internal class SendViewModel @Inject constructor( private val eventStateFactory = SendEventStateFactory( clickIntents = this, currentStateProvider = Provider { uiState }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeStateFactory = feeStateFactory, ) - private val amountNotificationFactory = AmountNotificationFactory( - currentStateProvider = Provider { uiState }, - stateRouterProvider = Provider { stateRouter }, - clickIntents = this, - ) - private val feeNotificationFactory = FeeNotificationFactory( currentStateProvider = Provider { uiState }, stateRouterProvider = Provider { stateRouter }, @@ -163,6 +156,7 @@ internal class SendViewModel @Inject constructor( private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + feePaidCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, stateRouterProvider = Provider { stateRouter }, @@ -185,20 +179,19 @@ internal class SendViewModel @Inject constructor( private var isTapHelpPreviewEnabled: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() - private var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null private var balanceJobHolder = JobHolder() private var balanceHidingJobHolder = JobHolder() - private var recipientsJobHolder = JobHolder() private var feeJobHolder = JobHolder() private var addressValidationJobHolder = JobHolder() private var memoValidationJobHolder = JobHolder() private var sendNotificationsJobHolder = JobHolder() private var feeNotificationsJobHolder = JobHolder() - private var amountNotificationsJobHolder = JobHolder() private var qrScannerJobHolder = JobHolder() private var sendIdleTimer = 0L + private var feeIdleTimer = 0L init { subscribeOnCurrencyStatusUpdates() @@ -329,12 +322,12 @@ internal class SendViewModel @Inject constructor( private suspend fun getFeeCurrencyStatusSync( cryptoCurrencyStatus: CryptoCurrencyStatus, isMultiCurrency: Boolean, - ): CryptoCurrencyStatus { + ): CryptoCurrencyStatus? { return if (isMultiCurrency) { getFeePaidCryptoCurrencyStatusSyncUseCase( userWalletId = userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull() ?: error("Fee currency is unreachable") + ).getOrNull() } else { cryptoCurrencyStatus } @@ -355,7 +348,7 @@ internal class SendViewModel @Inject constructor( private fun onDataLoaded( currencyStatus: CryptoCurrencyStatus, coinCurrencyStatus: CryptoCurrencyStatus, - feeCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, ) { cryptoCurrencyStatus = currencyStatus coinCryptoCurrencyStatus = coinCurrencyStatus @@ -380,62 +373,68 @@ internal class SendViewModel @Inject constructor( } private fun getWalletsAndRecent() { - combine( - flow = getUserWallets().conflate(), - flow2 = getTxHistory().conflate(), - ) { wallets, txHistory -> - uiState = stateFactory.onLoadedRecipientList( - wallets = wallets, - txHistory = txHistory, - ) + getUserWallets() + viewModelScope.launch(dispatchers.main) { + getTxHistory() } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(recipientsJobHolder) } - private fun getUserWallets(): Flow> { - return getWalletsUseCase() + private fun getUserWallets() { + getWalletsUseCase() + .conflate() .distinctUntilChanged() - .map { userWallets -> + .onEach { userWallets -> coroutineScope { - userWallets - .filterNot { it.walletId == userWalletId || it.isLocked } - .map { wallet -> - async(dispatchers.io) { - getCryptoCurrenciesUseCase.getSync(wallet.walletId) - .fold( - ifRight = { currencyItem -> - val walletCurrency = currencyItem.firstOrNull { - it.network.id == cryptoCurrency.network.id - } ?: return@fold null - val addresses = walletManagersFacade.getAddress( - userWalletId = wallet.walletId, - network = walletCurrency.network, - ) - return@fold addresses.firstOrNull()?.let { - AvailableWallet( - name = wallet.name, - address = it.value, - ) - } - }, - ifLeft = { null }, - ) - } - } - }.awaitAll() + runCatching { + userWallets + .filterNot { it.walletId == userWalletId || it.isLocked } + .map { wallet -> + async(dispatchers.io) { wallet.toAvailableWallet() } + }.awaitAll() + }.onSuccess { result -> + uiState = stateFactory.onLoadedWalletsList(wallets = result) + }.onFailure { + uiState = stateFactory.onLoadedWalletsList(wallets = emptyList()) + } + } } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) } - private fun getTxHistory(): Flow> { - return getFixedTxHistoryItemsUseCase( + private suspend fun UserWallet.toAvailableWallet(): AvailableWallet? { + return if (!isMultiCurrency) { + val status = getCryptoCurrencyStatusSyncUseCase(walletId).getOrNull() + val address = status?.value?.networkAddress.takeIf { + status?.currency?.network?.id == cryptoCurrency.network.id + } + address?.let { + AvailableWallet( + name = name, + address = it.defaultAddress.value, + ) + } + } else { + val statuses = getCryptoCurrencyStatusesSyncUseCase(walletId).getOrNull() + val walletCurrency = statuses?.firstOrNull { + it.currency.network.id == cryptoCurrency.network.id + } + val address = walletCurrency?.value?.networkAddress + address?.let { + AvailableWallet( + name = name, + address = it.defaultAddress.value, + ) + } + } + } + + private suspend fun getTxHistory() { + val txHistoryList = getFixedTxHistoryItemsUseCase.getSync( userWalletId = userWalletId, currency = cryptoCurrency, - ).fold( - ifRight = { it.distinctUntilChanged() }, - ifLeft = { emptyFlow() }, - ) + ).getOrElse { emptyList() } + uiState = stateFactory.onLoadedHistoryList(txHistory = txHistoryList) } private fun onStateActive() { @@ -443,7 +442,7 @@ internal class SendViewModel @Inject constructor( .onEach { when (it.type) { SendUiStateType.Fee -> loadFee() - SendUiStateType.Send -> sendIdleTimer = System.currentTimeMillis() + SendUiStateType.Send -> sendIdleTimer = SystemClock.elapsedRealtime() else -> Unit } } @@ -465,21 +464,11 @@ internal class SendViewModel @Inject constructor( .conflate() .distinctUntilChanged() .onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) } - .flowOn(dispatchers.io) + .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(feeNotificationsJobHolder) } - private fun updateAmountNotifications() { - amountNotificationFactory.create() - .conflate() - .distinctUntilChanged() - .onEach { uiState = amountStateFactory.getAmountNotificationState(notifications = it) } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(amountNotificationsJobHolder) - } - // region screen state navigation override fun popBackStack() = stateRouter.popBackStack() override fun onBackClick() { @@ -533,7 +522,15 @@ internal class SendViewModel @Inject constructor( ) return true } - return false + return checkIfFeeTooHigh( + state = uiState, + onShow = { diff -> + uiState = eventStateFactory.getFeeTooHighAlert( + diff = diff, + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + }, + ) } private fun onFeeCoverageAlert(): Boolean { @@ -572,12 +569,14 @@ internal class SendViewModel @Inject constructor( // endregion // region recipient state clicks - fun onRecipientAddressScanned(address: String) { + fun onQrCodeScanned(address: String) { viewModelScope.launch(dispatchers.main) { parseQrCodeUseCase(address, cryptoCurrency).fold( ifRight = { parsedCode -> onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode) - parsedCode.amount?.let { onAmountValueChange(it.toPlainString()) } + parsedCode.amount?.let { + onAmountValueChange(it.parseBigDecimal(decimals = cryptoCurrency.decimals)) + } parsedCode.memo?.let { onRecipientMemoValueChange(it) } }, ifLeft = { @@ -618,7 +617,9 @@ internal class SendViewModel @Inject constructor( network = cryptoCurrency.network, address = value, ).getOrElse { false } - onEnteredValidAddress(isValidAddress) + val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses + ?.any { it.value == value } ?: true + onEnteredValidAddress(isValidAddress, isAddressInWallet) return isValidAddress } @@ -632,25 +633,21 @@ internal class SendViewModel @Inject constructor( } ?: false } - private fun onEnteredValidAddress(isValidAddress: Boolean) { - val recipientState = uiState.recipientState ?: return - uiState = uiState.copy( - recipientState = recipientState.copy( - recent = recipientState.recent.map { it.copy(isVisible = !isValidAddress) }.toPersistentList(), - wallets = recipientState.wallets.map { it.copy(isVisible = !isValidAddress) }.toPersistentList(), - ), + private fun onEnteredValidAddress(isValidAddress: Boolean, isAddressInWallet: Boolean) { + uiState = stateFactory.getHiddenRecentListState( + isAddressInWallet = isAddressInWallet, + isValidAddress = isValidAddress, ) } private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) { val isRecent = type == EnterAddressSource.RecentAddress - val isAddressOnly = uiState.recipientState?.memoTextField == null - if (isRecent && isAddressOnly && isValidAddress) onNextClick() + if (isRecent && isValidAddress) onNextClick() } // endregion // region fee - override fun feeReload() = loadFee() + override fun feeReload(isToNextState: Boolean) = loadFee(isToNextState = isToNextState) override fun onFeeSelectorClick(feeType: FeeType) { uiState = feeStateFactory.onFeeSelectedState(feeType) @@ -682,6 +679,9 @@ internal class SendViewModel @Inject constructor( } private fun loadFee(isToNextState: Boolean = false) { + // debouncing fee request + if (SystemClock.elapsedRealtime() - feeIdleTimer < FEE_UPDATE_DELAY) return + viewModelScope.launch(dispatchers.main) { val isShowStatus = uiState.feeState?.fee == null if (isShowStatus) { @@ -689,26 +689,37 @@ internal class SendViewModel @Inject constructor( } val result = callFeeUseCase()?.fold( ifRight = { + feeIdleTimer = SystemClock.elapsedRealtime() uiState = feeStateFactory.onFeeOnLoadedState(it) if (isToNextState && !onFeeCoverageAlert()) { stateRouter.showSend() } }, ifLeft = { - if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState() + onFeeLoadFailed(isShowStatus, isToNextState) }, ) - if (result == null && isShowStatus) { - uiState = feeStateFactory.onFeeOnErrorState() + if (result == null) { + onFeeLoadFailed(isShowStatus, isToNextState) } updateFeeNotifications() - updateAmountNotifications() }.saveIn(feeJobHolder) .invokeOnCompletion { uiState = amountStateFactory.getOnAmountFeeLoadingCancel() } } + private fun onFeeLoadFailed(isShowStatus: Boolean, isToNextState: Boolean) { + when { + isToNextState -> { + uiState = eventStateFactory.getFeeUnreachableErrorState { + uiState = eventStateFactory.onConsumeEventState() + } + } + isShowStatus -> uiState = feeStateFactory.onFeeOnErrorState() + } + } + private suspend fun checkIfSubtractAvailable() { isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrency).fold( ifRight = { it }, @@ -736,12 +747,13 @@ internal class SendViewModel @Inject constructor( if (sendState.isSuccess) popBackStack() uiState = stateFactory.getSendingStateUpdate(isSending = true) - if (System.currentTimeMillis() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) { + if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) { verifyAndSendTransaction() } else { onCheckFeeUpdate() + feeIdleTimer = SystemClock.elapsedRealtime() + sendIdleTimer = SystemClock.elapsedRealtime() } - sendIdleTimer = System.currentTimeMillis() } override fun showAmount() { @@ -777,10 +789,9 @@ internal class SendViewModel @Inject constructor( analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked) } - override fun onAmountReduceClick(reducedAmount: String, clazz: Class) { - uiState = amountStateFactory.getOnAmountValueChange(reducedAmount) + override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class) { + uiState = amountStateFactory.getOnAmountValueChange(reducedAmount.parseBigDecimal(cryptoCurrency.decimals)) uiState = sendNotificationFactory.dismissNotificationState(clazz) - onCheckFeeUpdate() } override fun onNotificationCancel(clazz: Class) { @@ -834,13 +845,21 @@ internal class SendViewModel @Inject constructor( }, ifRight = { uiState = stateFactory.getSendingStateUpdate(isSending = false) - uiState = stateFactory.getTransactionSendState(txData) + updateTransactionStatus(txData) scheduleBalanceUpdate() analyticsEventHandler.send(SendAnalyticEvents.TransactionScreenOpened) }, ) } + private suspend fun updateTransactionStatus(txData: TransactionData) { + val txUrl = getExplorerTransactionUrlUseCase( + userWalletId = userWalletId, + network = cryptoCurrency.network, + ).getOrElse { "" } + uiState = stateFactory.getTransactionSendState(txData, txUrl) + } + private fun scheduleBalanceUpdate() { viewModelScope.launch(dispatchers.io) { delay(BALANCE_UPDATE_DELAY) @@ -873,8 +892,7 @@ internal class SendViewModel @Inject constructor( }, ifLeft = { uiState = stateFactory.getSendingStateUpdate(isSending = false) - eventStateFactory.getGenericErrorState( - error = (it as? GetFeeError.DataError)?.cause, + eventStateFactory.getFeeUnreachableErrorState( onConsume = { uiState = eventStateFactory.onConsumeEventState() }, ) }, @@ -884,7 +902,7 @@ internal class SendViewModel @Inject constructor( feeUpdatedState } else { uiState = stateFactory.getSendingStateUpdate(isSending = false) - eventStateFactory.getGenericErrorState( + eventStateFactory.getFeeUnreachableErrorState( onConsume = { uiState = eventStateFactory.onConsumeEventState() }, ) } @@ -900,13 +918,14 @@ internal class SendViewModel @Inject constructor( } // endregion - companion object { - private const val CHECK_FEE_UPDATE_DELAY = 60_000L - private const val BALANCE_UPDATE_DELAY = 10_000L + private companion object { + const val CHECK_FEE_UPDATE_DELAY = 60_000L + const val FEE_UPDATE_DELAY = 10_000L + const val BALANCE_UPDATE_DELAY = 10_000L - private const val RU_LOCALE = "ru" - private const val EN_LOCALE = "en" - private const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/" - private const val FEE_READ_MORE_URL_SECOND_PART = "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/" + const val RU_LOCALE = "ru" + const val EN_LOCALE = "en" + const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/" + const val FEE_READ_MORE_URL_SECOND_PART = "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/" } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 08b31e9a2e..6ab50afce3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -7,6 +7,7 @@ import com.tangem.blockchain.common.AmountType 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.minimalAmount import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository @@ -433,7 +434,7 @@ internal class SwapInteractorImpl @Inject constructor( ) { val isTezos = fromTokenStatus.currency.network.id.value == Blockchain.Tezos.id if (isTezos && amount.value == fromTokenStatus.value.amount) { - warnings.add(Warning.ReduceAmountWarning(TEZOS_FEE_THRESHOLD)) + warnings.add(Warning.ReduceAmountWarning(Blockchain.Tezos.minimalAmount())) } } @@ -1406,10 +1407,10 @@ internal class SwapInteractorImpl @Inject constructor( private fun Fee.getGasLimit(): Int { return when (this) { - is Fee.Common -> 0 is Fee.Ethereum -> gasLimit.toInt() is Fee.VeChain -> gasLimit.toInt() is Fee.Aptos -> gasLimit.toInt() + else -> 0 } } @@ -1595,6 +1596,5 @@ internal class SwapInteractorImpl @Inject constructor( private const val INCREASE_GAS_LIMIT_BY = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% private const val INFINITY_SYMBOL = "∞" - private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01") } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index e87a8f3c3c..9e5149bf68 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -57,6 +57,21 @@ internal object TokenDetailsPreviewData { ), ) + val tokenInfoBlockStateWithLongNameNoStandard = TokenInfoBlockState( + name = "Tether (USDT) with long name test", + iconState = TokenInfoBlockState.IconState.TokenIcon( + url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + fallbackTint = Color.Cyan, + fallbackBackground = Color.Blue, + isGrayscale = false, + ), + currency = TokenInfoBlockState.Currency.Token( + standardName = null, + networkIcon = R.drawable.img_shibarium_22, + networkName = "Shibarium", + ), + ) + val tokenInfoBlockState = TokenInfoBlockState( name = "Tether USDT", iconState = TokenInfoBlockState.IconState.CustomTokenIcon( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt index 7c1e1eda2b..f0ec9e8d43 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt @@ -19,7 +19,7 @@ internal data class TokenInfoBlockState( * @param networkIcon - token's network icon. */ data class Token( - val standardName: String, + val standardName: String?, val networkName: String, @DrawableRes val networkIcon: Int, ) : Currency() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index a88c945c5f..575565370e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig @@ -40,7 +41,7 @@ internal class TokenDetailsSkeletonStateConverter( currency = when (value) { is CryptoCurrency.Coin -> TokenInfoBlockState.Currency.Native is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token( - standardName = value.network.standardType.name, + standardName = value.network.standardType.getSpecifiedNameOrNull(), networkName = value.network.name, networkIcon = value.networkIconResId, ) @@ -65,6 +66,9 @@ internal class TokenDetailsSkeletonStateConverter( ) } + private fun Network.StandardType.getSpecifiedNameOrNull(): String? = + name.takeIf { this !is Network.StandardType.Unspecified } + private fun createMenu(cryptoCurrency: CryptoCurrency): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( items = buildList { if (featureToggles.isGenerateXPubEnabled() && isBitcoin(cryptoCurrency.network.id.value)) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt index 3254286ed2..3982a8dc21 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -105,13 +105,22 @@ private const val SEPARATOR = " %image% " @Composable private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): ExtractedTokenNetworkText { - val splitString = stringResource( - id = R.string.token_details_token_type_subtitle, - formatArgs = arrayOf( - tokenCurrency.standardName, - tokenCurrency.networkName, - ), - ).split(SEPARATOR) + val splitString = if (tokenCurrency.standardName != null) { + stringResource( + id = R.string.token_details_token_type_subtitle, + formatArgs = arrayOf( + tokenCurrency.standardName, + tokenCurrency.networkName, + ), + ).split(SEPARATOR) + } else { + stringResource( + id = R.string.token_details_token_type_subtitle_no_standard, + formatArgs = arrayOf( + tokenCurrency.networkName, + ), + ).split(SEPARATOR) + } return remember(splitString) { ExtractedTokenNetworkText( @@ -153,5 +162,6 @@ private class TokenInfoStateProvider : CollectionPreviewParameterProvider = mapOf(), + ) : AnalyticsEvent(category = "Token", event = event, params = params) { + + class PolkadotAccountReset(hasReset: Boolean) : Token( + event = "Polkadot Account Reset", + params = mapOf( + AnalyticsParam.STATE to if (hasReset) "Yes" else "No", + ), + ) + + class PolkadotImmortalTransactions(hasImmortalTransaction: Boolean) : Token( + event = "Polkadot Immortal Transactions", + params = mapOf( + AnalyticsParam.STATE to if (hasImmortalTransaction) "Yes" else "No", + ), + ) + } + sealed class MainScreen( event: String, params: Map = mapOf(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt index caa4c9fd87..4153d69e68 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt @@ -37,6 +37,7 @@ class BackupValidator @Inject constructor() { private fun validateBackupStatus(cardDTO: CardDTO): Boolean { val backupStatus = cardDTO.backupStatus - return backupStatus != null && backupStatus !is CardDTO.BackupStatus.CardLinked + backupStatus ?: return true // for card with null backup status, validation should always returns true + return backupStatus !is CardDTO.BackupStatus.CardLinked } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index df92e03ecc..0a3310ce92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -28,6 +29,7 @@ internal class MultiWalletContentLoader( private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, private val walletFeatureToggles: WalletFeatureToggles, + private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -42,6 +44,7 @@ internal class MultiWalletContentLoader( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, walletFeatureToggles = walletFeatureToggles, applyTokenListSortingUseCase = applyTokenListSortingUseCase, + runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ), MultiWalletWarningsSubscriber( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index cebb20d25f..13dbadebb3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -26,6 +27,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletFeatureToggles: WalletFeatureToggles, + private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { @@ -41,6 +43,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, applyTokenListSortingUseCase = applyTokenListSortingUseCase, walletFeatureToggles = walletFeatureToggles, + runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 525f4952ae..655c6255a3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -24,6 +25,7 @@ internal class SingleWalletWithTokenContentLoader( private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, private val getCardTokensListUseCase: GetCardTokensListUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -36,6 +38,7 @@ internal class SingleWalletWithTokenContentLoader( walletWithFundsChecker = walletWithFundsChecker, getCardTokensListUseCase = getCardTokensListUseCase, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, + runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ), MultiWalletWarningsSubscriber( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index e92241ad9a..e00b6fe74c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -21,6 +22,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val getCardTokensListUseCase: GetCardTokensListUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, + private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { @@ -34,6 +36,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( getCardTokensListUseCase = getCardTokensListUseCase, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, + runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index d4bc6a472f..cf4c25c45f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -6,7 +6,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.getOrElse +import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -33,6 +35,7 @@ internal abstract class BasicTokenListSubscriber( private val tokenListAnalyticsSender: TokenListAnalyticsSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) : WalletSubscriber() { private val sendAnalyticsJobHolder = JobHolder() @@ -53,6 +56,8 @@ internal abstract class BasicTokenListSubscriber( coroutineScope.launch { onTokenListReceived(maybeTokenList) }.saveIn(onTokenListReceivedJobHolder) + + coroutineScope.launch { startCheck(maybeTokenList) } }, flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(), transform = { maybeTokenList, maybeAppCurrency -> @@ -78,6 +83,21 @@ internal abstract class BasicTokenListSubscriber( ) } + private suspend fun startCheck(maybeTokenList: Lce) { + // Run Polkadot account health check + maybeTokenList.getOrNull()?.let { tokenList -> + val cryptoCurrencies = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) + is TokenList.Ungrouped -> tokenList.currencies + is TokenList.Empty -> emptyList() + } + + cryptoCurrencies.forEach { + runPolkadotAccountHealthCheckUseCase(userWallet.walletId, it.currency.network) + } + } + } + protected open suspend fun onTokenListReceived(maybeTokenList: Lce) { /* no-op */ } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index b4529f9513..62576cf73a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -6,6 +6,7 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.toLce import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.TokenList @@ -28,6 +29,7 @@ internal class MultiWalletTokenListSubscriber( tokenListAnalyticsSender: TokenListAnalyticsSender, walletWithFundsChecker: WalletWithFundsChecker, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -35,6 +37,7 @@ internal class MultiWalletTokenListSubscriber( tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, + runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) { override fun tokenListFlow(): LceFlow { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index 1431aff860..886cc1495e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -6,6 +6,7 @@ import com.tangem.domain.core.utils.toLce import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker @@ -22,6 +23,7 @@ internal class SingleWalletWithTokenListSubscriber( tokenListAnalyticsSender: TokenListAnalyticsSender, walletWithFundsChecker: WalletWithFundsChecker, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -29,6 +31,7 @@ internal class SingleWalletWithTokenListSubscriber( tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, + runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) { override fun tokenListFlow(): LceFlow = getCardTokensListUseCase(userWallet.walletId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 1ff5dc6191..553dafc78b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -107,7 +107,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) { - analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main)) + analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScreensSources.Main)) analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped) viewModelScope.launch(dispatchers.main) { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 9ce9f25864..c58b88a4a5 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -86,9 +86,9 @@ decompose = "2.2.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-598" +tangemBlockchainSdk = "develop-601" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-344" +tangemCardSdk = "develop-345" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt index 17d38bd0c6..ff8c190e0e 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt @@ -79,6 +79,7 @@ internal object BlockchainSDKConfigConverter : Converter "mantle" Blockchain.Flare, Blockchain.FlareTestnet -> "flare-networks" Blockchain.Taraxa, Blockchain.TaraxaTestnet -> "taraxa" - Blockchain.Base, Blockchain.BaseTestnet -> "base" + Blockchain.Base, Blockchain.BaseTestnet -> "base-ethereum" Blockchain.Koinos, Blockchain.KoinosTestnet -> "koinos" } } @@ -333,7 +333,6 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, - Blockchain.Playa3ull, Blockchain.Nexa, Blockchain.NexaTestnet, Blockchain.Radiant, diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index a6e7acf14d..cf10f1ce54 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -32,4 +32,9 @@ object BlockchainUtils { val blockchain = Blockchain.fromId(networkId) return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet } + + fun isDogecoin(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Dogecoin + } } \ No newline at end of file