From 8c0712850971a0767a70262ce634932d493ad688 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Feb 2024 18:21:47 +0300 Subject: [PATCH 01/33] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 4 + .../src/main/res/drawable/ic_settings_24.xml | 9 ++ features/qr-scanning/impl/build.gradle.kts | 1 + .../feature/qrscanning/QrScanningFragment.kt | 16 +++- .../CameraDeniedBottomSheetConfig.kt | 8 ++ .../QrScanningCameraDeniedBottomSheet.kt | 82 +++++++++++++++++++ .../presentation/QrScanningContent.kt | 26 +++--- .../presentation/QrScanningState.kt | 4 +- .../presentation/QrScanningStateController.kt | 35 ++++++++ .../DismissBottomSheetTransformer.kt | 11 +++ .../InitializeQrScanningStateTransformer.kt} | 17 ++-- .../transformers/QrScanningTransformer.kt | 8 ++ .../ShowCameraDeniedBottomSheetTransformer.kt | 24 ++++++ .../viewmodel/BaseQrScanningClickIntents.kt | 33 ++++++++ .../viewmodel/QrScanningClickIntents.kt | 43 ++++++++++ .../viewmodel/QrScanningViewModel.kt | 56 +++++-------- 16 files changed, 316 insertions(+), 61 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_settings_24.xml create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraDeniedBottomSheetConfig.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateController.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/DismissBottomSheetTransformer.kt rename features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/{QrScanningStateFactory.kt => transformers/InitializeQrScanningStateTransformer.kt} (57%) create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/QrScanningTransformer.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt create mode 100644 features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 052973065a..23e26fd59a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -393,6 +393,10 @@ By balance Organize tokens Ungroup + Select from the gallery + Settings + Camera access denied + You have not given access to your camera %1$s %2$s address on %3$s network %1$s (%2$s) on %3$s network Send only %s to this address. Sending any other currency will result in its irreversible loss. diff --git a/core/ui/src/main/res/drawable/ic_settings_24.xml b/core/ui/src/main/res/drawable/ic_settings_24.xml new file mode 100644 index 0000000000..480a877aa2 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_settings_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts index 712e2a79ce..e1d37506b4 100644 --- a/features/qr-scanning/impl/build.gradle.kts +++ b/features/qr-scanning/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.compose) /** Camera */ implementation(deps.camera.camera2) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt index 5805460bc2..a3ba7da2d9 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt @@ -2,6 +2,7 @@ package com.tangem.feature.qrscanning import android.Manifest import android.content.pm.PackageManager +import android.net.Uri import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable @@ -10,6 +11,8 @@ import androidx.compose.ui.graphics.Color import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.mlkit.vision.common.InputImage import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder @@ -45,12 +48,19 @@ internal class QrScanningFragment : ComposeFragment() { } private val cameraPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { - if (!it) parentFragmentManager.popBackStack() + if (!it) viewModel.onCameraDeniedState() + } + private val galleryLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { + val selectedImage = it ?: Uri.EMPTY + if (selectedImage != Uri.EMPTY) { + val image = InputImage.fromFilePath(requireContext(), selectedImage) + analyzer.analyze(image) + } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel.router = innerRouter + viewModel.setRouter(innerRouter, galleryLauncher) cameraExecutor = Executors.newSingleThreadExecutor() requestCameraPermission() } @@ -70,7 +80,7 @@ internal class QrScanningFragment : ComposeFragment() { QrScanningContent( executor = { cameraExecutor }, analyzer = { analyzer }, - uiState = viewModel.uiState, + uiState = viewModel.uiState.collectAsStateWithLifecycle().value, ) setFitSystemWindows(fit = false) } diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraDeniedBottomSheetConfig.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraDeniedBottomSheetConfig.kt new file mode 100644 index 0000000000..3d23eb1813 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/CameraDeniedBottomSheetConfig.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.qrscanning.presentation + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +data class CameraDeniedBottomSheetConfig( + val onGalleryClick: () -> Unit, + val onCancelClick: () -> Unit, +) : TangemBottomSheetConfigContent \ 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 new file mode 100644 index 0000000000..a4ca7198dc --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt @@ -0,0 +1,82 @@ +package com.tangem.feature.qrscanning.presentation + +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.compose.foundation.layout.Column +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.core.content.ContextCompat +import com.tangem.core.ui.components.SimpleSettingsRow +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.qrscanning.impl.R + +@Composable +fun CameraDeniedBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet(config) { content: CameraDeniedBottomSheetConfig -> + CameraDeniedBottomSheet(content = content) + } +} + +@Composable +private fun CameraDeniedBottomSheet(content: CameraDeniedBottomSheetConfig) { + val context = LocalContext.current + Column { + CameraDeniedBottomSheetHeader() + SimpleSettingsRow( + title = stringResource(id = R.string.qr_scanner_camera_denied_settings_button), + icon = R.drawable.ic_settings_24, + onItemsClick = { + val intent: Intent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null), + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ContextCompat.startActivity(context, intent, null) + }, + ) + SimpleSettingsRow( + title = stringResource(id = R.string.qr_scanner_camera_denied_gallery_button), + icon = R.drawable.ic_gallery_24, + onItemsClick = content.onGalleryClick, + ) + SimpleSettingsRow( + title = stringResource(id = R.string.common_close), + icon = R.drawable.ic_close, + onItemsClick = content.onCancelClick, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), + ) + } +} + +@Composable +private fun CameraDeniedBottomSheetHeader() { + Text( + text = stringResource(id = R.string.qr_scanner_camera_denied_title), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing20, + end = TangemTheme.dimens.spacing20, + top = TangemTheme.dimens.spacing16, + ), + ) + Text( + text = stringResource(id = R.string.qr_scanner_camera_denied_text), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing20, + end = TangemTheme.dimens.spacing20, + top = TangemTheme.dimens.spacing3, + bottom = TangemTheme.dimens.spacing16, + ), + ) +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt index 8d169703a6..6dc9d3592b 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt @@ -1,9 +1,10 @@ package com.tangem.feature.qrscanning.presentation -import android.net.Uri -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.* +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.Canvas import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -18,14 +19,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Constraints -import com.google.mlkit.vision.common.InputImage import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIconContent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference @@ -44,15 +43,9 @@ internal fun QrScanningContent( analyzer: () -> MLKitBarcodeAnalyzer, uiState: QrScanningState, ) { - val context = LocalContext.current var isFlash by remember { mutableStateOf(false) } - val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { - val selectedImage = it ?: Uri.EMPTY - if (selectedImage != Uri.EMPTY) { - val image = InputImage.fromFilePath(context, selectedImage) - analyzer().analyze(image) - } - } + + BackHandler(onBack = uiState.onBackClick) Box( modifier = Modifier @@ -101,13 +94,16 @@ internal fun QrScanningContent( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false), - onClick = { galleryLauncher.launch("image/*") }, + onClick = uiState.onGalleryClick, ), tint = TangemColorPalette.White, ) } }, ) + if (uiState.bottomSheetConfig != null) { + CameraDeniedBottomSheet(uiState.bottomSheetConfig) + } } } diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt index d9eb27b155..e9ca30f4e1 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt @@ -1,6 +1,7 @@ package com.tangem.feature.qrscanning.presentation import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference @Immutable @@ -8,5 +9,6 @@ data class QrScanningState( val message: TextReference?, val onQrScanned: (String) -> Unit, val onBackClick: () -> Unit, - val onGalleryClicked: () -> Unit, + val onGalleryClick: () -> Unit, + val bottomSheetConfig: TangemBottomSheetConfig? = null, ) \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateController.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateController.kt new file mode 100644 index 0000000000..efad8e7984 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateController.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.qrscanning.presentation + +import com.tangem.feature.qrscanning.presentation.transformers.QrScanningTransformer +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class QrScanningStateController @Inject constructor() { + + val uiState: StateFlow get() = mutableUiState + + val value: QrScanningState get() = uiState.value + + private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) + + fun update(function: (QrScanningState) -> QrScanningState) { + mutableUiState.update(function = function) + } + + fun update(transformer: QrScanningTransformer) { + mutableUiState.update(function = transformer::transform) + } + + private fun getInitialState(): QrScanningState { + return QrScanningState( + message = null, + onBackClick = {}, + onQrScanned = {}, + onGalleryClick = {}, + ) + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/DismissBottomSheetTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/DismissBottomSheetTransformer.kt new file mode 100644 index 0000000000..bd3d2fb621 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/DismissBottomSheetTransformer.kt @@ -0,0 +1,11 @@ +package com.tangem.feature.qrscanning.presentation.transformers + +import com.tangem.feature.qrscanning.presentation.QrScanningState + +internal class DismissBottomSheetTransformer : QrScanningTransformer { + override fun transform(prevState: QrScanningState): QrScanningState { + return prevState.copy( + bottomSheetConfig = null, + ) + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateFactory.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt similarity index 57% rename from features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateFactory.kt rename to features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt index 0b7ef2c4da..c4df653e5a 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateFactory.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt @@ -1,16 +1,19 @@ -package com.tangem.feature.qrscanning.presentation +package com.tangem.feature.qrscanning.presentation.transformers import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.feature.qrscanning.viewmodel.QrScanningClickIntents import com.tangem.feature.qrscanning.SourceType import com.tangem.feature.qrscanning.impl.R +import com.tangem.feature.qrscanning.presentation.QrScanningState +import com.tangem.feature.qrscanning.viewmodel.QrScanningClickIntents -internal class QrScanningStateFactory( - val clickIntents: QrScanningClickIntents, -) { +internal class InitializeQrScanningStateTransformer( + private val clickIntents: QrScanningClickIntents, + private val source: SourceType, + private val network: String?, +) : QrScanningTransformer { - fun getInitialState(source: SourceType, network: String?): QrScanningState { + override fun transform(prevState: QrScanningState): QrScanningState { val message = when (source) { SourceType.SEND -> network?.let { resourceReference(R.string.send_qrcode_scan_info, wrappedList(it)) } else -> null @@ -20,7 +23,7 @@ internal class QrScanningStateFactory( message = message, onBackClick = clickIntents::onBackClick, onQrScanned = clickIntents::onQrScanned, - onGalleryClicked = clickIntents::onGalleryClicked, + onGalleryClick = clickIntents::onGalleryClicked, ) } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/QrScanningTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/QrScanningTransformer.kt new file mode 100644 index 0000000000..16dcdfbd77 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/QrScanningTransformer.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.qrscanning.presentation.transformers + +import com.tangem.feature.qrscanning.presentation.QrScanningState + +internal interface QrScanningTransformer { + + fun transform(prevState: QrScanningState): QrScanningState +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt new file mode 100644 index 0000000000..5677a023f8 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/ShowCameraDeniedBottomSheetTransformer.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.qrscanning.presentation.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.feature.qrscanning.presentation.CameraDeniedBottomSheetConfig +import com.tangem.feature.qrscanning.presentation.QrScanningState +import com.tangem.feature.qrscanning.viewmodel.QrScanningClickIntents + +internal class ShowCameraDeniedBottomSheetTransformer( + private val clickIntents: QrScanningClickIntents, +) : QrScanningTransformer { + + override fun transform(prevState: QrScanningState): QrScanningState { + return prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = clickIntents::onBackClick, + content = CameraDeniedBottomSheetConfig( + onCancelClick = clickIntents::onBackClick, + onGalleryClick = clickIntents::onGalleryClicked, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt new file mode 100644 index 0000000000..3ae564a745 --- /dev/null +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt @@ -0,0 +1,33 @@ +package com.tangem.feature.qrscanning.viewmodel + +import androidx.activity.result.ActivityResultLauncher +import com.tangem.feature.qrscanning.SourceType +import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter +import kotlinx.coroutines.CoroutineScope +import kotlin.properties.Delegates + +internal open class BaseQrScanningClickIntents { + + protected val router: QrScanningInnerRouter get() = _router + protected val viewModelScope: CoroutineScope get() = _viewModelScope + protected val source: SourceType get() = _source + protected val galleryLauncher: ActivityResultLauncher get() = _galleryLauncher + + private var _router: QrScanningInnerRouter by Delegates.notNull() + private var _viewModelScope: CoroutineScope by Delegates.notNull() + private var _source: SourceType by Delegates.notNull() + + private var _galleryLauncher: ActivityResultLauncher by Delegates.notNull() + + open fun initialize( + router: QrScanningInnerRouter, + source: SourceType, + galleryLauncher: ActivityResultLauncher, + coroutineScope: CoroutineScope, + ) { + _router = router + _viewModelScope = coroutineScope + _source = source + _galleryLauncher = galleryLauncher + } +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt index 38219460a6..7ecbdae562 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt @@ -1,5 +1,13 @@ package com.tangem.feature.qrscanning.viewmodel +import com.tangem.feature.qrscanning.presentation.QrScanningStateController +import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer +import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.launch +import javax.inject.Inject + interface QrScanningClickIntents { fun onBackClick() @@ -7,4 +15,39 @@ interface QrScanningClickIntents { fun onQrScanned(qrCode: String) fun onGalleryClicked() +} + +@ViewModelScoped +internal class QrScanningClickIntentsImplementor @Inject constructor( + private val stateHolder: QrScanningStateController, + private val emitQrScannedEventUseCase: EmitQrScannedEventUseCase, + private val dispatcher: CoroutineDispatcherProvider, +) : BaseQrScanningClickIntents(), QrScanningClickIntents { + + private var isScanned = false + + override fun onBackClick() = router.popBackStack() + + override fun onQrScanned(qrCode: String) { + if (qrCode.isNotBlank()) { + if (!isScanned) { + router.popBackStack() + isScanned = true + } + viewModelScope.launch(dispatcher.main) { + emitQrScannedEventUseCase.invoke(source, qrCode) + } + } + } + + override fun onGalleryClicked() { + galleryLauncher.launch(GALLERY_IMAGE_FILTER) + if (stateHolder.value.bottomSheetConfig != null) { + stateHolder.update(DismissBottomSheetTransformer()) + } + } + + companion object { + private const val GALLERY_IMAGE_FILTER = "image/*" + } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt index b0eef6d35c..5402f469b0 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt @@ -1,8 +1,6 @@ package com.tangem.feature.qrscanning.viewmodel -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -11,50 +9,38 @@ import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY import com.tangem.feature.qrscanning.SourceType import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import com.tangem.feature.qrscanning.presentation.QrScanningState -import com.tangem.feature.qrscanning.presentation.QrScanningStateFactory -import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase -import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.feature.qrscanning.presentation.QrScanningStateController +import com.tangem.feature.qrscanning.presentation.transformers.ShowCameraDeniedBottomSheetTransformer +import com.tangem.feature.qrscanning.presentation.transformers.InitializeQrScanningStateTransformer import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.StateFlow import javax.inject.Inject -import kotlin.properties.Delegates @HiltViewModel internal class QrScanningViewModel @Inject constructor( - private val emitQrScannedEventUseCase: EmitQrScannedEventUseCase, - private val dispatcher: CoroutineDispatcherProvider, + private val stateHolder: QrScanningStateController, + private val clickIntents: QrScanningClickIntentsImplementor, savedStateHandle: SavedStateHandle, -) : ViewModel(), QrScanningClickIntents { +) : ViewModel() { private val source: SourceType = savedStateHandle[SOURCE_KEY] ?: error("Source is mandatory") private val network: String? = savedStateHandle[NETWORK_KEY] - private val factory = QrScanningStateFactory( - clickIntents = this, - ) + val uiState: StateFlow = stateHolder.uiState - var router: QrScanningInnerRouter by Delegates.notNull() - - var uiState: QrScanningState by mutableStateOf(factory.getInitialState(source, network)) - private set - - private var isScanned = false - - override fun onBackClick() = router.popBackStack() - - override fun onQrScanned(qrCode: String) { - if (qrCode.isNotBlank()) { - if (!isScanned) { - router.popBackStack() - isScanned = true - } - viewModelScope.launch(dispatcher.main) { - emitQrScannedEventUseCase.invoke(source, qrCode) - } - } + fun setRouter(router: QrScanningInnerRouter, galleryLauncher: ActivityResultLauncher) { + clickIntents.initialize( + router = router, + source = source, + galleryLauncher = galleryLauncher, + coroutineScope = viewModelScope, + ) + stateHolder.update(InitializeQrScanningStateTransformer(clickIntents, source, network)) } - override fun onGalleryClicked() { - // [REDACTED_JIRA] + fun onQrScanned(qrCode: String) = clickIntents.onQrScanned(qrCode) + + fun onCameraDeniedState() { + stateHolder.update(ShowCameraDeniedBottomSheetTransformer(clickIntents)) } } \ No newline at end of file From fa6d35c218aa251e435a7adf2f6247c84e16ac0d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 16 Feb 2024 17:14:51 +0300 Subject: [PATCH 02/33] Updated on 2026-08-14 --- .../presentation/viewmodel/SendViewModel.kt | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) 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 fa912092d9..c202ee727b 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 @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import androidx.paging.PagingData +import androidx.paging.cachedIn import arrow.core.Either import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData @@ -348,15 +349,17 @@ internal class SendViewModel @Inject constructor( } private fun getTxHistory(): Flow> { - return flow { - txHistoryItemsUseCase( - userWalletId = userWalletId, - currency = cryptoCurrency, - ).fold( - ifRight = { emitAll(it.distinctUntilChanged()) }, - ifLeft = {}, - ) - } + return txHistoryItemsUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + ).fold( + ifRight = { + it.distinctUntilChanged().cachedIn(viewModelScope) + }, + ifLeft = { + emptyFlow() + }, + ) } private fun getTxHistoryCount(): Flow { From 1a9f22a4aec2d27870ef94269a3b8e2a8ca02f73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Feb 2024 16:21:09 +0700 Subject: [PATCH 03/33] Updated on 2026-08-14 --- .../tangem/feature/swap/ui/StateBuilder.kt | 26 +++++++ .../feature/swap/viewmodels/SwapViewModel.kt | 75 ++++++++++++++++++- 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 69289418b4..49d98a51dd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -693,6 +693,32 @@ internal class StateBuilder( ) } + fun updateSendCurrencyBalance( + uiState: SwapStateHolder, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): SwapStateHolder { + if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState + + return uiState.copy( + sendCardData = uiState.sendCardData.copy( + balance = cryptoCurrencyStatus.getFormattedAmount(isNeedSymbol = false), + ), + ) + } + + fun updateReceiveCurrencyBalance( + uiState: SwapStateHolder, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): SwapStateHolder { + if (uiState.receiveCardData !is SwapCardState.SwapCardData) return uiState + + return uiState.copy( + receiveCardData = uiState.receiveCardData.copy( + balance = cryptoCurrencyStatus.getFormattedAmount(isNeedSymbol = false), + ), + ) + } + fun updateBalanceHiddenState(uiState: SwapStateHolder, isBalanceHidden: Boolean): SwapStateHolder { if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState if (uiState.receiveCardData !is SwapCardState.SwapCardData) return uiState diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index d194494046..be2e616805 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* +import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -13,11 +14,13 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor @@ -62,6 +65,7 @@ internal class SwapViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -104,6 +108,9 @@ internal class SwapViewModel @Inject constructor( (it.error is DataError.ExchangeTooSmallAmountError || it.error is DataError.ExchangeTooBigAmountError) } + private val fromTokenBalanceJobHolder = JobHolder() + private val toTokenBalanceJobHolder = JobHolder() + val currentScreen: SwapNavScreen get() = swapRouter.currentScreen @@ -173,6 +180,23 @@ internal class SwapViewModel @Inject constructor( state, ), ) + + val userWalletId = swapInteractor.getSelectedWallet()?.walletId ?: return@launch + (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { + subscribeToCoinBalanceUpdates( + userWalletId = userWalletId, + coin = it, + isFromCurrency = true, + ) + } + + (dataState.toCryptoCurrency?.currency as? CryptoCurrency.Coin)?.let { + subscribeToCoinBalanceUpdates( + userWalletId = userWalletId, + coin = it, + isFromCurrency = false, + ) + } }.onFailure { Timber.tag(loggingTag).e(it) @@ -617,7 +641,6 @@ internal class SwapViewModel @Inject constructor( unavailable = unavailable, afterSearch = true, ), - ) } else { tokenDataState.copy( @@ -647,15 +670,35 @@ internal class SwapViewModel @Inject constructor( analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = true, token = it)) } + val userWalletId = swapInteractor.getSelectedWallet()?.walletId + if (foundToken != null) { val fromToken: CryptoCurrencyStatus val toToken: CryptoCurrencyStatus if (isOrderReversed) { fromToken = foundToken.currencyStatus toToken = initialCryptoCurrencyStatus + + val newToken = fromToken.currency as? CryptoCurrency.Coin + if (userWalletId != null && newToken != null) { + subscribeToCoinBalanceUpdates( + userWalletId = userWalletId, + coin = newToken, + isFromCurrency = true, + ) + } } else { fromToken = initialCryptoCurrencyStatus toToken = foundToken.currencyStatus + + val newToken = toToken.currency as? CryptoCurrency.Coin + if (userWalletId != null && newToken != null) { + subscribeToCoinBalanceUpdates( + userWalletId = userWalletId, + coin = newToken, + isFromCurrency = false, + ) + } } dataState = dataState.copy( fromCryptoCurrency = fromToken, @@ -673,6 +716,36 @@ internal class SwapViewModel @Inject constructor( } } + private fun subscribeToCoinBalanceUpdates( + userWalletId: UserWalletId, + coin: CryptoCurrency.Coin, + isFromCurrency: Boolean, + ) { + Timber.d("Subscribe to ${coin.id} balance updates") + + getCurrencyStatusUpdatesUseCase( + userWalletId = userWalletId, + currencyId = coin.id, + isSingleWalletWithTokens = false, + ) + .mapNotNull { (it as? Either.Right)?.value } + .distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes + .onEach { + Timber.d("${coin.id} balance is ${it.value.amount}") + + uiState = if (isFromCurrency) { + dataState = dataState.copy(fromCryptoCurrency = it) + stateBuilder.updateSendCurrencyBalance(uiState, it) + } else { + dataState = dataState.copy(toCryptoCurrency = it) + stateBuilder.updateReceiveCurrencyBalance(uiState, it) + } + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + .saveIn(if (isFromCurrency) fromTokenBalanceJobHolder else toTokenBalanceJobHolder) + } + private fun onChangeCardsClicked() { val newFromToken = dataState.toCryptoCurrency val newToToken = dataState.fromCryptoCurrency From 3399ffbcd2d4a8c82c4fe4e08a90e63d49ad06d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 15 Feb 2024 14:16:47 +0300 Subject: [PATCH 04/33] Updated on 2026-08-14 --- app/build.gradle.kts | 4 ++++ app/src/main/AndroidManifest.xml | 3 +-- core/datasource/build.gradle.kts | 4 ++++ core/datasource/src/main/AndroidManifest.xml | 3 +-- core/featuretoggles/build.gradle.kts | 4 ++++ .../src/main/AndroidManifest.xml | 2 -- core/res/build.gradle.kts | 4 ++++ core/res/src/main/AndroidManifest.xml | 2 -- core/ui/build.gradle.kts | 4 ++++ core/ui/src/main/AndroidManifest.xml | 2 -- data/source/preferences/build.gradle.kts | 4 ++++ .../preferences/src/main/AndroidManifest.xml | 2 -- domain/legacy/build.gradle.kts | 4 ++++ domain/legacy/src/main/AndroidManifest.xml | 4 ---- features/onboarding/build.gradle.kts | 4 ++++ .../onboarding/src/main/AndroidManifest.xml | 2 -- features/referral/data/build.gradle.kts | 4 ++++ .../data/src/main/AndroidManifest.xml | 2 -- .../domain/src/main/AndroidManifest.xml | 2 -- .../referral/presentation/build.gradle.kts | 4 ++++ .../presentation/src/main/AndroidManifest.xml | 2 -- features/swap/api/build.gradle.kts | 4 ++++ .../swap/api/src/main/AndroidManifest.xml | 2 -- features/swap/data/build.gradle.kts | 4 ++++ .../swap/data/src/main/AndroidManifest.xml | 2 -- features/swap/presentation/build.gradle.kts | 4 ++++ .../presentation/src/main/AndroidManifest.xml | 2 -- features/tester/api/build.gradle.kts | 4 ++++ .../tester/api/src/main/AndroidManifest.xml | 2 -- features/tester/impl/build.gradle.kts | 4 ++++ .../tester/impl/src/main/AndroidManifest.xml | 3 +-- features/wallet/api/build.gradle.kts | 4 ++++ .../wallet/api/src/main/AndroidManifest.xml | 2 -- features/wallet/impl/build.gradle.kts | 4 ++++ .../wallet/impl/src/main/AndroidManifest.xml | 4 ---- .../wallet/ui/components/WalletsList.kt | 12 ++--------- gradle.properties | 1 + gradle/dependencies.toml | 20 +++++++++---------- gradle/wrapper/gradle-wrapper.properties | 12 +++++------ libs/auth/build.gradle.kts | 4 ++++ libs/auth/src/main/AndroidManifest.xml | 2 -- libs/crypto/build.gradle.kts | 4 ++++ libs/crypto/src/main/AndroidManifest.xml | 2 -- .../extension/AppExtensionConfigurations.kt | 2 ++ .../LibraryExtensionConfigurations.kt | 1 + 45 files changed, 101 insertions(+), 70 deletions(-) delete mode 100644 core/featuretoggles/src/main/AndroidManifest.xml delete mode 100644 core/res/src/main/AndroidManifest.xml delete mode 100644 core/ui/src/main/AndroidManifest.xml delete mode 100644 data/source/preferences/src/main/AndroidManifest.xml delete mode 100644 domain/legacy/src/main/AndroidManifest.xml delete mode 100644 features/onboarding/src/main/AndroidManifest.xml delete mode 100644 features/referral/data/src/main/AndroidManifest.xml delete mode 100644 features/referral/domain/src/main/AndroidManifest.xml delete mode 100644 features/referral/presentation/src/main/AndroidManifest.xml delete mode 100644 features/swap/api/src/main/AndroidManifest.xml delete mode 100644 features/swap/data/src/main/AndroidManifest.xml delete mode 100644 features/swap/presentation/src/main/AndroidManifest.xml delete mode 100644 features/tester/api/src/main/AndroidManifest.xml delete mode 100644 features/wallet/api/src/main/AndroidManifest.xml delete mode 100644 features/wallet/impl/src/main/AndroidManifest.xml delete mode 100644 libs/auth/src/main/AndroidManifest.xml delete mode 100644 libs/crypto/src/main/AndroidManifest.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3cf379b69f..2010e7ac69 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,6 +9,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.wallet" +} + configurations.all { exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18") exclude(group = "com.github.komputing.kethereum") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 99e803e45b..4b57ad94ed 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 522cd3257e..e4239e40d6 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -6,6 +6,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.datasource" +} + dependencies { /** Project */ diff --git a/core/datasource/src/main/AndroidManifest.xml b/core/datasource/src/main/AndroidManifest.xml index a87322003a..f1b5bc5af0 100644 --- a/core/datasource/src/main/AndroidManifest.xml +++ b/core/datasource/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/core/featuretoggles/build.gradle.kts b/core/featuretoggles/build.gradle.kts index 5354b55813..d775494d39 100644 --- a/core/featuretoggles/build.gradle.kts +++ b/core/featuretoggles/build.gradle.kts @@ -6,6 +6,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.core.featuretoggles" +} + dependencies { /** DI */ diff --git a/core/featuretoggles/src/main/AndroidManifest.xml b/core/featuretoggles/src/main/AndroidManifest.xml deleted file mode 100644 index f96c64f1db..0000000000 --- a/core/featuretoggles/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/core/res/build.gradle.kts b/core/res/build.gradle.kts index 1fbece85d5..c2fce27feb 100644 --- a/core/res/build.gradle.kts +++ b/core/res/build.gradle.kts @@ -2,4 +2,8 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) id("configuration") +} + +android { + namespace = "com.tangem.core.res" } \ No newline at end of file diff --git a/core/res/src/main/AndroidManifest.xml b/core/res/src/main/AndroidManifest.xml deleted file mode 100644 index 1420909c0e..0000000000 --- a/core/res/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 62f9551fa1..e2abfa0f4b 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -4,6 +4,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.core.ui" +} + dependencies { /** Project - Common */ implementation(projects.common) diff --git a/core/ui/src/main/AndroidManifest.xml b/core/ui/src/main/AndroidManifest.xml deleted file mode 100644 index c5a36c7e17..0000000000 --- a/core/ui/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/data/source/preferences/build.gradle.kts b/data/source/preferences/build.gradle.kts index 7cd9e54515..3768a33a8c 100644 --- a/data/source/preferences/build.gradle.kts +++ b/data/source/preferences/build.gradle.kts @@ -5,6 +5,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.data.source.preferences" +} + dependencies { implementation(deps.androidx.core.ktx) implementation(deps.moshi) diff --git a/data/source/preferences/src/main/AndroidManifest.xml b/data/source/preferences/src/main/AndroidManifest.xml deleted file mode 100644 index 67568f1b9e..0000000000 --- a/data/source/preferences/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index 9a4c2225c2..bc0e716a7a 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -4,6 +4,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.domain.features" +} + dependencies { implementation(project(":core:datasource")) implementation(project(":core:utils")) diff --git a/domain/legacy/src/main/AndroidManifest.xml b/domain/legacy/src/main/AndroidManifest.xml deleted file mode 100644 index ea7f515204..0000000000 --- a/domain/legacy/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index 8318108615..6629611e63 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -7,6 +7,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.feature.onboarding" +} + dependencies { /** Core modules */ implementation(project(":common")) diff --git a/features/onboarding/src/main/AndroidManifest.xml b/features/onboarding/src/main/AndroidManifest.xml deleted file mode 100644 index 21015a0f53..0000000000 --- a/features/onboarding/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/features/referral/data/build.gradle.kts b/features/referral/data/build.gradle.kts index 76c384d6d6..5125499148 100644 --- a/features/referral/data/build.gradle.kts +++ b/features/referral/data/build.gradle.kts @@ -6,6 +6,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.feature.referral.data" +} + dependencies { /** Project */ diff --git a/features/referral/data/src/main/AndroidManifest.xml b/features/referral/data/src/main/AndroidManifest.xml deleted file mode 100644 index 6f8fe3941b..0000000000 --- a/features/referral/data/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/features/referral/domain/src/main/AndroidManifest.xml b/features/referral/domain/src/main/AndroidManifest.xml deleted file mode 100644 index ace8d8e355..0000000000 --- a/features/referral/domain/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 280c994188..0a90fdc329 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -6,6 +6,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.feature.referral.presentation" +} + dependencies { /** Core modules */ implementation(project(":core:analytics")) diff --git a/features/referral/presentation/src/main/AndroidManifest.xml b/features/referral/presentation/src/main/AndroidManifest.xml deleted file mode 100644 index 4c5a0a1f54..0000000000 --- a/features/referral/presentation/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/features/swap/api/build.gradle.kts b/features/swap/api/build.gradle.kts index 882b05bd82..76bb25817f 100644 --- a/features/swap/api/build.gradle.kts +++ b/features/swap/api/build.gradle.kts @@ -7,6 +7,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.feature.swap.api" +} + dependencies { /** DI */ implementation(deps.hilt.android) diff --git a/features/swap/api/src/main/AndroidManifest.xml b/features/swap/api/src/main/AndroidManifest.xml deleted file mode 100644 index b46e5ddb0b..0000000000 --- a/features/swap/api/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 12693598d2..208635ebbd 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -6,6 +6,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.feature.swap.data" +} + dependencies { /** AndroidX */ diff --git a/features/swap/data/src/main/AndroidManifest.xml b/features/swap/data/src/main/AndroidManifest.xml deleted file mode 100644 index 5981b0aeee..0000000000 --- a/features/swap/data/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index a08a338a18..8986bf7e0d 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -7,6 +7,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.feature.swap.presentation" +} + dependencies { /** Core modules */ implementation(projects.core.analytics) diff --git a/features/swap/presentation/src/main/AndroidManifest.xml b/features/swap/presentation/src/main/AndroidManifest.xml deleted file mode 100644 index 927ad53a71..0000000000 --- a/features/swap/presentation/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/features/tester/api/build.gradle.kts b/features/tester/api/build.gradle.kts index 1fbece85d5..0456459cad 100644 --- a/features/tester/api/build.gradle.kts +++ b/features/tester/api/build.gradle.kts @@ -2,4 +2,8 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) id("configuration") +} + +android { + namespace = "com.tangem.features.tester.api" } \ No newline at end of file diff --git a/features/tester/api/src/main/AndroidManifest.xml b/features/tester/api/src/main/AndroidManifest.xml deleted file mode 100644 index 99ac562598..0000000000 --- a/features/tester/api/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 9bc0022b59..86db03dca7 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -6,6 +6,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.feature.tester.impl" +} + dependencies { /** AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/tester/impl/src/main/AndroidManifest.xml b/features/tester/impl/src/main/AndroidManifest.xml index b4e4fc0cfd..b729d8f73c 100644 --- a/features/tester/impl/src/main/AndroidManifest.xml +++ b/features/tester/impl/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + - \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index e2d5cb6d0f..db4d102187 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -6,6 +6,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.feature.wallet.impl" +} + dependencies { /** AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/wallet/impl/src/main/AndroidManifest.xml b/features/wallet/impl/src/main/AndroidManifest.xml deleted file mode 100644 index a1a96bfa00..0000000000 --- a/features/wallet/impl/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 455cf810c4..bd417d7a27 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData @@ -31,8 +30,6 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.common.Wallet import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList -private const val SHORT_SNAP_ELEMENT_COUNT = 50 - /** * Wallets list component * @@ -56,7 +53,7 @@ internal fun WalletsList( state = lazyListState, contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState, itemWidth = itemWidth), + flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState), ) { items( items = wallets, @@ -76,17 +73,14 @@ internal fun WalletsList( /** * Custom implementation of fling behaviour that overrides 'shortSnapVelocityThreshold'. - * Every user's drag action will similar to a short snap - * if drag offset is less than [SHORT_SNAP_ELEMENT_COUNT] * item width. * * @param lazyListState lazy list state - * @param itemWidth list item width * * @see rememberSnapFlingBehavior */ @OptIn(ExperimentalFoundationApi::class) @Composable -private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState, itemWidth: Dp): SnapFlingBehavior { +private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState): SnapFlingBehavior { val snappingLayout = remember(lazyListState) { SnapLayoutInfoProvider(lazyListState) } val density = LocalDensity.current val highVelocityApproachSpec: DecayAnimationSpec = rememberSplineBasedDecay() @@ -97,8 +91,6 @@ private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState, itemWidt lowVelocityAnimationSpec = tween(durationMillis = 1000, easing = LinearEasing), highVelocityAnimationSpec = highVelocityApproachSpec, snapAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow), - density = density, - shortSnapVelocityThreshold = itemWidth * SHORT_SNAP_ELEMENT_COUNT, ) } } diff --git a/gradle.properties b/gradle.properties index 4131a81f9d..7218026460 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,3 +15,4 @@ org.gradle.daemon = true android.useAndroidX = true android.enableJetifier = true org.gradle.unsafe.configuration-cache = true +android.nonTransitiveRClass = false diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 56e74e1713..f4b5153615 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -3,10 +3,10 @@ [versions] # region Classpath -androidGradlePlugin = "7.4.2" +androidGradlePlugin = "8.2.2" firebaseCrashlytics = "2.9.4" googleServices = "4.3.10" -kotlin = "1.8.21" +kotlin = "1.9.22" # endregion Classpath # region AndroidX @@ -24,17 +24,17 @@ androidx-datastore = "1.0.0" # endregion AndroidX # region Compose -compose-compiler = "1.4.7" -compose-runtime = "1.5.3" -compose-foundation = "1.5.3" -compose-material = "1.5.3" -compose-material3 = "1.1.2" +compose-compiler = "1.5.9" +compose-runtime = "1.6.1" +compose-foundation = "1.6.1" +compose-material = "1.6.1" +compose-material3 = "1.2.0" compose-constraint = "1.0.1" -compose-navigation = "2.7.4" +compose-navigation = "2.7.7" compose-accompanist = "0.30.1" compose-paging = "3.2.1" compose-reorderable = "0.9.6" -compose-lifecycle-runtime = "2.6.2" +compose-lifecycle-runtime = "2.7.0" # endregion Compose # region Other libraries @@ -50,7 +50,7 @@ googleMaterialComponent = "1.6.1" googlePlayCore = "1.10.3" googlePlayCoreKtx = "1.8.1" googlePlayServicesWallet = "19.1.0" -hilt = "2.44" +hilt = "2.46" hilt-navigation = "1.0.0" jodatime = "2.12.1" kotlin-immutable-collections = "0.3.5" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 135c32a8de..f5e4b2a14c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Mar 07 14:45:01 MSK 2023 -distributionBase = GRADLE_USER_HOME -distributionUrl = https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip -distributionPath = wrapper/dists -zipStorePath = wrapper/dists -zipStoreBase = GRADLE_USER_HOME +#Tue Feb 13 17:04:37 MSK 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/libs/auth/build.gradle.kts b/libs/auth/build.gradle.kts index 1fbece85d5..e1341f668a 100644 --- a/libs/auth/build.gradle.kts +++ b/libs/auth/build.gradle.kts @@ -2,4 +2,8 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) id("configuration") +} + +android { + namespace = "com.tangem.lib.auth" } \ No newline at end of file diff --git a/libs/auth/src/main/AndroidManifest.xml b/libs/auth/src/main/AndroidManifest.xml deleted file mode 100644 index e314bdebe7..0000000000 --- a/libs/auth/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index 917e9c310d..9a8ccaea31 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -6,6 +6,10 @@ plugins { id("configuration") } +android { + namespace = "com.tangem.lib.crypto" +} + dependencies { /** Coroutines */ diff --git a/libs/crypto/src/main/AndroidManifest.xml b/libs/crypto/src/main/AndroidManifest.xml deleted file mode 100644 index 1fd7238d41..0000000000 --- a/libs/crypto/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index 05b2b465c2..f288d89a7d 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -35,6 +35,8 @@ private fun AppExtension.configureDefaultConfig(project: Project) { AppConfig.versionName } + buildFeatures.buildConfig = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt index bc18afa9b9..053151f9f1 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt @@ -21,6 +21,7 @@ private fun LibraryExtension.configureDefaultConfig() { vectorDrawables { useSupportLibrary = true } + buildFeatures.buildConfig = true } } From 61bbf0fd67e9c7d527805424e73b639d997433d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 13 Feb 2024 17:16:17 +0300 Subject: [PATCH 05/33] Updated on 2026-08-14 --- .../marketprice/MarketPriceBlock.kt | 11 +++++++++ .../marketprice/PriceChangeState.kt | 5 ---- .../components/marketprice/PriceChangeType.kt | 6 +++++ .../marketprice/utils/PriceChangeConverter.kt | 21 ++++++++++++++++ core/ui/src/main/res/drawable/ic_elipse_8.xml | 9 +++++++ .../managetokens/state/QuotesState.kt | 5 +--- .../factory/QuotesToQuotesStateConverter.kt | 8 +++---- .../ManageTokensStatePreviewData.kt | 1 + .../previewdata/TokenItemStatePreviewData.kt | 24 ++++++++++++++++++- .../ui/components/PriceChangesChart.kt | 10 ++++++++ .../ui/components/TokenPriceChange.kt | 5 +++- .../ui/components/TokenRowItem.kt | 9 ++++--- .../TokenDetailsLoadedBalanceConverter.kt | 6 ++--- .../common/component/token/TokenPrice.kt | 3 +++ .../SingleWalletMarketPriceConverter.kt | 6 ++--- .../converter/TokenItemStateConverter.kt | 5 ++-- 16 files changed, 105 insertions(+), 29 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeType.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/marketprice/utils/PriceChangeConverter.kt create mode 100644 core/ui/src/main/res/drawable/ic_elipse_8.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 1626a42bce..efbf7b9f2a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -163,11 +163,13 @@ private fun PriceChangeInPercent(config: PriceChangeState.Content) { id = when (type) { PriceChangeType.UP -> R.drawable.ic_arrow_up_8 PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 + PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 }, ), tint = when (type) { PriceChangeType.UP -> TangemTheme.colors.icon.accent PriceChangeType.DOWN -> TangemTheme.colors.icon.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive }, contentDescription = null, ) @@ -177,6 +179,7 @@ private fun PriceChangeInPercent(config: PriceChangeState.Content) { color = when (type) { PriceChangeType.UP -> TangemTheme.colors.text.accent PriceChangeType.DOWN -> TangemTheme.colors.text.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled }, style = TangemTheme.typography.body2, ) @@ -251,6 +254,14 @@ private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterPr type = PriceChangeType.UP, ), ), + MarketPriceBlockState.Content( + currencySymbol = "BTC", + price = "98900 $", + priceChangeConfig = PriceChangeState.Content( + valueInPercent = "0.00%", + type = PriceChangeType.NEUTRAL, + ), + ), MarketPriceBlockState.Loading(currencySymbol = "BTC"), MarketPriceBlockState.Error(currencySymbol = "BTC"), ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt index 5130e77203..8db483f50c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt @@ -5,9 +5,4 @@ sealed class PriceChangeState { data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState() object Unknown : PriceChangeState() -} - -/** Price changing type */ -enum class PriceChangeType { - UP, DOWN } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeType.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeType.kt new file mode 100644 index 0000000000..de78fc1368 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeType.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.components.marketprice + +/** Price changing type */ +enum class PriceChangeType { + UP, DOWN, NEUTRAL, +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/utils/PriceChangeConverter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/utils/PriceChangeConverter.kt new file mode 100644 index 0000000000..5d8bb93cb3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/utils/PriceChangeConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.core.ui.components.marketprice.utils + +import com.tangem.core.ui.components.marketprice.PriceChangeType +import java.math.BigDecimal +import java.math.RoundingMode + +object PriceChangeConverter { + + fun fromBigDecimal(value: BigDecimal?, scale: Int = 2): PriceChangeType { + val formattedValue = value + ?.movePointRight(2) + ?.setScale(scale, RoundingMode.HALF_UP) + ?: return PriceChangeType.NEUTRAL + + return when (formattedValue.signum()) { + 1 -> PriceChangeType.UP + -1 -> PriceChangeType.DOWN + else -> PriceChangeType.NEUTRAL + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_elipse_8.xml b/core/ui/src/main/res/drawable/ic_elipse_8.xml new file mode 100644 index 0000000000..dab2aad1bb --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_elipse_8.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt index 9e255ebc77..3518626707 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.state +import com.tangem.core.ui.components.marketprice.PriceChangeType import kotlinx.collections.immutable.ImmutableList internal sealed class QuotesState { @@ -10,8 +11,4 @@ internal sealed class QuotesState { val changeType: PriceChangeType, val chartData: ImmutableList, ) : QuotesState() -} - -enum class PriceChangeType { - UP, DOWN } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt index b9559d3a9c..972c004779 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt @@ -1,8 +1,9 @@ package com.tangem.managetokens.presentation.managetokens.state.factory +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.tokens.model.Quote -import com.tangem.managetokens.presentation.managetokens.state.PriceChangeType import com.tangem.managetokens.presentation.managetokens.state.QuotesState import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf @@ -16,19 +17,18 @@ internal class QuotesToQuotesStateConverter : Converter { priceChange = BigDecimalFormatter.formatPercent( percent = priceChange.movePointLeft(2), useAbsoluteValue = true, - maxFractionDigits = 1, - minFractionDigits = 1, ), changeType = priceChange.getPriceChangeType(), chartData = // TODO (in [REDACTED_TASK_KEY] when endpoint is ready) when (priceChange.getPriceChangeType()) { PriceChangeType.UP -> persistentListOf(0f, 5f, 10f, 30f) PriceChangeType.DOWN -> persistentListOf(15f, 12f, 13f, 18f, 10f, 3f) + PriceChangeType.NEUTRAL -> persistentListOf(0f, 0f, 0f, 0f) }, ) } private fun BigDecimal.getPriceChangeType(): PriceChangeType { - return if (this >= BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN + return PriceChangeConverter.fromBigDecimal(value = this) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt index 40d4334eb3..961253bf87 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt @@ -32,6 +32,7 @@ internal object ManageTokensStatePreviewData { get() = listOf( TokenItemStatePreviewData.loadedPriceDown, TokenItemStatePreviewData.loadedPriceUp, + TokenItemStatePreviewData.loadedPriceNeutral, ) private val searchState: SearchBarState diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt index 01ed9bdda9..1ba8f760e6 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt @@ -2,7 +2,11 @@ package com.tangem.managetokens.presentation.managetokens.state.previewdata import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color -import com.tangem.managetokens.presentation.managetokens.state.* +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.managetokens.presentation.managetokens.state.QuotesState +import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType +import com.tangem.managetokens.presentation.managetokens.state.TokenIconState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState import kotlinx.collections.immutable.persistentListOf internal object TokenItemStatePreviewData { @@ -46,6 +50,24 @@ internal object TokenItemStatePreviewData { chooseNetworkState = ChooseNetworkStatePreviewData.state, ) + val loadedPriceNeutral: TokenItemState + get() = TokenItemState.Loaded( + id = "BTC", + name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", + tokenId = "BTC", + currencySymbol = "BTC", + tokenIcon = tokenIconState, + quotes = QuotesState.Content( + priceChange = "0.00%", + changeType = PriceChangeType.NEUTRAL, + chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 10f), + ), + rate = "31 285.72$", + availableAction = mutableStateOf(TokenButtonType.ADD), + onButtonClick = {}, + chooseNetworkState = ChooseNetworkStatePreviewData.state, + ) + private val tokenIconState: TokenIconState get() = TokenIconState( iconReference = null, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt index 9909709a8d..998dbebc72 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt @@ -120,4 +120,14 @@ private fun Chart_Negative_Preview() { persistentListOf(10f, 2f, 4f, 1f, 5f), ) } +} + +@Preview(widthDp = 150, heightDp = 150, showBackground = true) +@Composable +private fun Chart_Neutral_Preview() { + TangemTheme(isDark = true) { + PriceChangesChart( + persistentListOf(5f, 2f, 4f, 1f, 5f), + ) + } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt index 1b2d04c7a9..28b7691794 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt @@ -11,9 +11,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.components.SpacerW4 +import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.PriceChangeType import com.tangem.managetokens.presentation.managetokens.state.QuotesState @Composable @@ -48,11 +48,13 @@ private fun PriceChangeIcon(type: PriceChangeType?) { id = when (animatedType) { PriceChangeType.UP -> R.drawable.ic_arrow_up_8 PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 + PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 }, ), tint = when (animatedType) { PriceChangeType.UP -> TangemTheme.colors.icon.accent PriceChangeType.DOWN -> TangemTheme.colors.icon.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive }, contentDescription = null, ) @@ -69,6 +71,7 @@ private fun PriceChangeText(type: PriceChangeType?, text: String?) { color = when (type) { PriceChangeType.UP -> TangemTheme.colors.text.accent PriceChangeType.DOWN -> TangemTheme.colors.text.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled null -> TangemTheme.colors.text.primary1 }, overflow = TextOverflow.Ellipsis, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt index 12a403e3e4..d92f0a8b81 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt @@ -180,7 +180,8 @@ private fun BaseSurface(modifier: Modifier = Modifier, content: @Composable () - } } -@Preview() +// region Preview +@Preview(showBackground = true, widthDp = 360) @Composable private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { TangemTheme(isDark = false) { @@ -188,7 +189,7 @@ private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::cla } } -@Preview +@Preview(showBackground = true, widthDp = 360) @Composable private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { TangemTheme(isDark = true) { @@ -201,5 +202,7 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider, @@ -118,9 +118,7 @@ internal class TokenDetailsLoadedBalanceConverter( } private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType { - val priceChange = status.priceChange ?: return PriceChangeType.DOWN - - return if (priceChange > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN + return PriceChangeConverter.fromBigDecimal(status.priceChange) } private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPrice.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPrice.kt index 06cd93ccec..4822e0d489 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPrice.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPrice.kt @@ -87,11 +87,13 @@ private fun PriceChangeIcon(type: PriceChangeType) { id = when (animatedType) { PriceChangeType.UP -> R.drawable.ic_arrow_up_8 PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 + PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 }, ), tint = when (animatedType) { PriceChangeType.UP -> TangemTheme.colors.icon.accent PriceChangeType.DOWN -> TangemTheme.colors.icon.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive }, contentDescription = null, ) @@ -106,6 +108,7 @@ private fun PriceChangeText(type: PriceChangeType?, text: String?, modifier: Mod color = when (type) { PriceChangeType.UP -> TangemTheme.colors.text.accent PriceChangeType.DOWN -> TangemTheme.colors.text.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled null -> TangemTheme.colors.text.tertiary }, overflow = TextOverflow.Ellipsis, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt index 60fcaa1c3c..157d0a1180 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -3,11 +3,11 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.converter.Converter -import java.math.BigDecimal internal class SingleWalletMarketPriceConverter( private val status: CryptoCurrencyStatus.Status, @@ -61,8 +61,6 @@ internal class SingleWalletMarketPriceConverter( } private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType { - val priceChange = status.priceChange ?: return PriceChangeType.DOWN - - return if (priceChange > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN + return PriceChangeConverter.fromBigDecimal(status.priceChange) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index 77aa101de4..aa168db655 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -97,8 +98,6 @@ internal class TokenItemStateConverter( priceChangePercent = BigDecimalFormatter.formatPercent( percent = priceChange, useAbsoluteValue = true, - maxFractionDigits = 1, - minFractionDigits = 1, ), type = priceChange.getPriceChangeType(), ) @@ -117,6 +116,6 @@ internal class TokenItemStateConverter( } private fun BigDecimal.getPriceChangeType(): PriceChangeType { - return if (this > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN + return PriceChangeConverter.fromBigDecimal(value = this) } } \ No newline at end of file From 6e642be3cac80a0f12e9071f77612d90ea6f85ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Feb 2024 21:14:45 +0700 Subject: [PATCH 06/33] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../main/java/com/tangem/tap/MainActivity.kt | 15 +- .../java/com/tangem/tap/TapApplication.kt | 16 +- .../com/tangem/tap/common/extensions/Store.kt | 7 + .../tap/common/redux/global/GlobalReducer.kt | 17 +- ...ultUserWalletsListManagerFeatureToggles.kt | 12 ++ ...rWalletsListManagerFeatureTogglesModule.kt | 23 +++ .../di/UserWalletsListManagerModule.kt | 108 +++++++++++++ .../di/UserWalletsListManagerProvider.kt | 2 +- .../GeneralUserWalletsListManager.kt | 146 ++++++++++++++++++ .../details/redux/DetailsMiddleware.kt | 67 ++++++-- .../saveWallet/redux/SaveWalletAction.kt | 2 + .../saveWallet/redux/SaveWalletMiddleware.kt | 61 +++++++- .../saveWallet/redux/SaveWalletReducer.kt | 6 +- .../saveWallet/ui/SaveWalletViewModel.kt | 9 +- .../tap/proxy/redux/DaggerGraphState.kt | 4 + .../configs/feature_toggles_config.json | 4 + .../features/ExampleInstrumentedTest.kt | 22 --- .../UserWalletsListManagerFeatureToggles.kt | 6 + 19 files changed, 478 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/DefaultUserWalletsListManagerFeatureToggles.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerFeatureTogglesModule.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt delete mode 100644 domain/legacy/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerFeatureToggles.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2010e7ac69..4d977ecce4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -116,6 +116,7 @@ dependencies { implementation(deps.androidx.core.ktx) implementation(deps.androidx.core.splashScreen) implementation(deps.androidx.appCompat) + implementation(deps.androidx.datastore) implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.constraintLayout) implementation(deps.androidx.activity.compose) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 04b3f17541..116e6dcf12 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -36,6 +36,8 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles +import com.tangem.domain.wallets.legacy.asLockable import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.send.api.navigation.SendRouter @@ -140,6 +142,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var deepLinksRegistry: DeepLinksRegistry + @Inject + lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles + + @Inject + lateinit var generalUserWalletsListManager: UserWalletsListManager + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -438,7 +446,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { - val canSaveWallets = userWalletsListManager is BiometricUserWalletsListManager + val canSaveWallets = if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) { + runCatching { userWalletsListManager.asLockable()?.isLockedSync } + .fold(onSuccess = { true }, onFailure = { false }) + } else { + userWalletsListManager is BiometricUserWalletsListManager + } val hasSavedWallets = userWalletsListManager.hasUserWallets if (canSaveWallets && hasSavedWallets) { diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index e4337c01f6..5ca956a2bf 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -35,6 +35,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles @@ -155,6 +156,12 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var accountCreator: AccountCreator + + @Inject + lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles + + @Inject + lateinit var generalUserWalletsListManager: UserWalletsListManager // endregion Injected override fun onCreate() { @@ -183,8 +190,13 @@ internal class TapApplication : Application(), ImageLoaderFactory { // TODO: Try to performance and user experience. // [REDACTED_JIRA] runBlocking { - initUserWalletsListManager() featureTogglesManager.init() + + if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) { + store.dispatch(GlobalAction.UpdateUserWalletsListManager(generalUserWalletsListManager)) + } else { + initUserWalletsListManager() + } } val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) @@ -232,6 +244,8 @@ internal class TapApplication : Application(), ImageLoaderFactory { sendFeatureToggles = sendFeatureToggles, blockchainDataStorage = blockchainDataStorage, accountCreator = accountCreator, + userWalletsListManagerFeatureToggles = userWalletsListManagerFeatureToggles, + generalUserWalletsListManager = generalUserWalletsListManager, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index 79c87a7f28..d16d3488e2 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -7,6 +7,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.Dispatchers @@ -87,4 +88,10 @@ fun Store<*>.dispatchOpenUrl(url: String) { fun Store<*>.dispatchShare(url: String) { dispatch(NavigationAction.Share(url)) +} + +inline fun Store.inject(getDependency: DaggerGraphState.() -> T?): T { + return requireNotNull(state.daggerGraphState.getDependency()) { + "${T::class.simpleName} isn't initialized " + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index bd03cf6cae..778a37590c 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -3,11 +3,13 @@ package com.tangem.tap.common.redux.global import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.utils.extensions.replaceBy import org.rekotlin.Action @@ -95,10 +97,17 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde ) } is GlobalAction.UpdateUserWalletsListManager -> { - appStateHolder.userWalletsListManager = action.manager - globalState.copy( - userWalletsListManager = action.manager, - ) + val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) + + if (featureToggles.isGeneralManagerEnabled) { + val generalUserWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + + appStateHolder.userWalletsListManager = generalUserWalletsListManager + globalState.copy(userWalletsListManager = generalUserWalletsListManager) + } else { + appStateHolder.userWalletsListManager = action.manager + globalState.copy(userWalletsListManager = action.manager) + } } is GlobalAction.ChangeAppThemeMode -> globalState.copy( appThemeMode = action.appThemeMode, diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/DefaultUserWalletsListManagerFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/DefaultUserWalletsListManagerFeatureToggles.kt new file mode 100644 index 0000000000..ca0f135159 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/DefaultUserWalletsListManagerFeatureToggles.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.domain.userWalletList + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles + +internal class DefaultUserWalletsListManagerFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : UserWalletsListManagerFeatureToggles { + + override val isGeneralManagerEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerFeatureTogglesModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerFeatureTogglesModule.kt new file mode 100644 index 0000000000..f94041dc73 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerFeatureTogglesModule.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.domain.userWalletList.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles +import com.tangem.tap.domain.userWalletList.DefaultUserWalletsListManagerFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object UserWalletsListManagerFeatureTogglesModule { + + @Provides + @Singleton + fun provideUserWalletsListManagerFeatureToggles( + featureTogglesManager: FeatureTogglesManager, + ): UserWalletsListManagerFeatureToggles { + return DefaultUserWalletsListManagerFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt new file mode 100644 index 0000000000..1a162862e6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -0,0 +1,108 @@ +package com.tangem.tap.domain.userWalletList.di + +import android.content.Context +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.common.authentication.AuthenticatedStorage +import com.tangem.common.json.TangemSdkAdapter +import com.tangem.common.services.secure.SecureStorage +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.sdk.storage.AndroidSecureStorage +import com.tangem.sdk.storage.createEncryptedSharedPreferences +import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager +import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager +import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager +import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager +import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator +import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository +import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository +import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository +import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository +import com.tangem.tap.domain.userWalletList.utils.json.* +import com.tangem.tap.tangemSdkManager +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object UserWalletsListManagerModule { + + @Provides + @Singleton + fun provideGeneralUserWalletsListManager( + @ApplicationContext applicationContext: Context, + appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, + ): UserWalletsListManager { + return GeneralUserWalletsListManager( + runtimeUserWalletsListManager = RuntimeUserWalletsListManager(), + biometricUserWalletsListManager = createBiometricUserWalletsListManager(applicationContext), + appPreferencesStore = appPreferencesStore, + dispatchers = dispatchers, + ) + } + + private fun createBiometricUserWalletsListManager(applicationContext: Context): UserWalletsListManager { + val moshi = Moshi.Builder() + .add(WalletDerivedKeysMapAdapter()) + .add(ScanResponseDerivedKeysMapAdapter()) + .add(ByteArrayKeyAdapter()) + .add(ExtendedPublicKeysMapAdapter()) + .add(CardBackupStatusAdapter()) + .add(DerivationPathAdapterWithMigration()) + .add(TangemSdkAdapter.DateAdapter()) + .add(TangemSdkAdapter.DerivationNodeAdapter()) + .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model + .add(KotlinJsonAdapterFactory()) + .build() + + val secureStorage = AndroidSecureStorage( + preferences = SecureStorage.createEncryptedSharedPreferences( + context = applicationContext, + storageName = USER_WALLETS_STORAGE_NAME, + ), + ) + + val authenticatedStorage = AuthenticatedStorage( + secureStorage = UserWalletsKeysStoreDecorator( + featureStorage = secureStorage, + cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage }, + ), + keystoreManager = DelegatedKeystoreManager( + keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager }, + ), + ) + + val keysRepository = BiometricUserWalletsKeysRepository( + moshi = moshi, + secureStorage = secureStorage, + authenticatedStorage = authenticatedStorage, + ) + + val publicInformationRepository = DefaultUserWalletsPublicInformationRepository( + moshi = moshi, + secureStorage = secureStorage, + ) + + val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository( + moshi = moshi, + secureStorage = secureStorage, + ) + + val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(secureStorage = secureStorage) + + return BiometricUserWalletsListManager( + keysRepository = keysRepository, + publicInformationRepository = publicInformationRepository, + sensitiveInformationRepository = sensitiveInformationRepository, + selectedUserWalletRepository = selectedUserWalletRepository, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt index 025cb4df17..efb44252c2 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt @@ -21,7 +21,7 @@ import com.tangem.tap.domain.userWalletList.utils.json.* import com.tangem.tap.tangemSdkManager import com.tangem.utils.Provider -private const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage" +internal const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage" fun UserWalletsListManager.Companion.provideBiometricImplementation( applicationContext: Context, 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 new file mode 100644 index 0000000000..cf7497ea51 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt @@ -0,0 +1,146 @@ +package com.tangem.tap.domain.userWalletList.implementation + +import com.tangem.common.CompletionResult +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.get +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import timber.log.Timber + +/** + * General implementation of [UserWalletsListManager] that helps to switch between Runtime and Biometric + * implementations. + * + * @property runtimeUserWalletsListManager runtime user wallets list manager + * @property biometricUserWalletsListManager biometric user wallets list manager + * @property appPreferencesStore app preferences store + * @property dispatchers coroutine dispatcher provider + * +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalCoroutinesApi::class) +internal class GeneralUserWalletsListManager( + private val runtimeUserWalletsListManager: UserWalletsListManager, + private val biometricUserWalletsListManager: UserWalletsListManager, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : UserWalletsListManager.Lockable { + + private val applicationScope = CoroutineScope(dispatchers.io) + private val implementation = MutableStateFlow(runtimeUserWalletsListManager) + + init { + subscribeOnCurrentManager() + } + + override val userWallets: Flow> + get() = implementation.flatMapLatest { it.userWallets } + + override val selectedUserWallet: Flow + get() = implementation.flatMapLatest { it.selectedUserWallet } + + override val selectedUserWalletSync: UserWallet? + get() = implementation.value.selectedUserWalletSync + + override val hasUserWallets: Boolean + get() = implementation.value.hasUserWallets + + override val walletsCount: Int + get() = implementation.value.walletsCount + + override val isLocked: Flow + get() = implementation.flatMapLatest { + if (it is UserWalletsListManager.Lockable) { + it.isLocked + } else { + error("RuntimeUserWalletsListManager is not lockable") + } + } + + override val isLockedSync: Boolean + get() { + val implementation = implementation.value + return if (implementation is UserWalletsListManager.Lockable) { + implementation.isLockedSync + } else { + error("RuntimeUserWalletsListManager is not lockable") + } + } + + override suspend fun select(userWalletId: UserWalletId): CompletionResult { + return implementation.value.select(userWalletId) + } + + override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult { + return implementation.value.save(userWallet, canOverride) + } + + override suspend fun update( + userWalletId: UserWalletId, + update: suspend (UserWallet) -> UserWallet, + ): CompletionResult { + return implementation.value.update(userWalletId, update) + } + + override suspend fun delete(userWalletIds: List): CompletionResult { + return implementation.value.delete(userWalletIds) + } + + override suspend fun clear(): CompletionResult { + return implementation.value.clear() + } + + override suspend fun get(userWalletId: UserWalletId): CompletionResult { + return implementation.value.get(userWalletId) + } + + override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult { + val implementation = implementation.value + return if (implementation is UserWalletsListManager.Lockable) { + implementation.unlock() + } else { + error("RuntimeUserWalletsListManager is not lockable") + } + } + + override fun lock() { + val implementation = implementation.value + return if (implementation is UserWalletsListManager.Lockable) { + implementation.lock() + } else { + error("RuntimeUserWalletsListManager is not lockable") + } + } + + private fun subscribeOnCurrentManager() { + appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) + .distinctUntilChanged() + .onEach { shouldSaveUserWallets -> + val manager = if (shouldSaveUserWallets) { + biometricUserWalletsListManager.copyFrom(runtimeUserWalletsListManager) + } else { + runtimeUserWalletsListManager.copyFrom(biometricUserWalletsListManager) + } + + Timber.d("Switch to ${manager::class.java.simpleName}") + + implementation.value = manager + } + .flowOn(dispatchers.io) + .launchIn(applicationScope) + } + + /** Copy data from [old] manager and clean it */ + private suspend fun UserWalletsListManager.copyFrom(old: UserWalletsListManager): UserWalletsListManager { + old.selectedUserWalletSync?.let { this.save(it) } + old.clear() + + return this + } +} \ No newline at end of file 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 508f29e664..3d68508c7e 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 @@ -24,10 +24,7 @@ import com.tangem.domain.wallets.legacy.isLockedSync import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings -import com.tangem.tap.common.extensions.dispatchDialogShow -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.onUserWalletSelected +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -292,7 +289,8 @@ class DetailsMiddleware { private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch { // Nothing to change - val walletsRepository = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + val walletsRepository = store.inject(DaggerGraphState::walletsRepository) + if (walletsRepository.shouldSaveUserWalletsSync() == enable) { store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) return@launch @@ -360,6 +358,19 @@ class DetailsMiddleware { private suspend fun saveCurrentWallet( scanResponse: ScanResponse?, enableAccessCodesSaving: Boolean, + ): CompletionResult { + val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) + + return if (featureToggles.isGeneralManagerEnabled) { + saveCurrentWalletByNewWay(scanResponse, enableAccessCodesSaving) + } else { + saveCurrentWalletByOldWay(scanResponse, enableAccessCodesSaving) + } + } + + private suspend fun saveCurrentWalletByOldWay( + scanResponse: ScanResponse?, + enableAccessCodesSaving: Boolean, ): CompletionResult { val userWallet = userWalletsListManager.selectedUserWalletSync ?: scanResponse?.let { UserWalletBuilder(it).build() } @@ -381,8 +392,26 @@ class DetailsMiddleware { Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On)) preferencesStorage.shouldShowSaveUserWalletScreen = false - store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) - .saveShouldSaveUserWallets(item = true) + store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) + } + .doOnFailure { error -> + Timber.e(error, "Unable to save user wallet") + } + } + + private suspend fun saveCurrentWalletByNewWay( + scanResponse: ScanResponse?, + enableAccessCodesSaving: Boolean, + ): CompletionResult { + store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) + + return if (enableAccessCodesSaving) { + saveAccessCodes(scanResponse) + } else { + CompletionResult.Success(Unit) + } + .doOnSuccess { + Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On)) } .doOnFailure { error -> Timber.e(error, "Unable to save user wallet") @@ -390,13 +419,22 @@ class DetailsMiddleware { } private suspend fun deleteSavedWalletsAndAccessCodes(): CompletionResult { + val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) + + return if (featureToggles.isGeneralManagerEnabled) { + deleteSavedWalletsAndAccessCodesByNewWay() + } else { + deleteSavedWalletsAndAccessCodesByOldWay() + } + } + + private suspend fun deleteSavedWalletsAndAccessCodesByOldWay(): CompletionResult { return userWalletsListManager.clear() .doOnSuccess { Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off)) deleteSavedAccessCodes() updateUserWalletsListManager(enableUserWalletsSaving = false) - store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) - .saveShouldSaveUserWallets(item = false) + store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home)) } @@ -405,6 +443,17 @@ class DetailsMiddleware { } } + private suspend fun deleteSavedWalletsAndAccessCodesByNewWay(): CompletionResult { + Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off)) + + deleteSavedAccessCodes() + store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) + + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home)) + + return CompletionResult.Success(Unit) + } + private fun saveAccessCodes(scanResponse: ScanResponse?): CompletionResult { Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On)) diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt index 00a5c7413c..934f7e8880 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt @@ -16,6 +16,8 @@ internal sealed interface SaveWalletAction : Action { data class Error(val error: TangemError) : SaveWalletAction } + object AllowToUseBiometrics : SaveWalletAction + object Dismiss : SaveWalletAction object CloseError : SaveWalletAction diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 9408cbcc9e..ddf1c22d74 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -18,6 +18,7 @@ import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Onboarding 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.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation @@ -42,6 +43,7 @@ internal class SaveWalletMiddleware { private fun handleAction(action: SaveWalletAction, state: SaveWalletState) { when (action) { is SaveWalletAction.Save -> saveWalletIfBiometricsEnrolled(state) + is SaveWalletAction.AllowToUseBiometrics -> allowToUseBiometrics(state) is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics() is SaveWalletAction.SaveWalletWasShown -> saveWalletWasShown() is SaveWalletAction.Dismiss -> dismiss(state) @@ -96,11 +98,16 @@ internal class SaveWalletMiddleware { .build() ?: return@launch - provideLockableUserWalletsListManagerIfNot() + val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) + if (featureToggles.isGeneralManagerEnabled) { + store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) + } else { + provideLockableUserWalletsListManagerIfNot() + } val isFirstSavedWallet = !userWalletsListManager.hasUserWallets - saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet) + saveAccessCodeIfNeeded(accessCode = state.backupInfo?.accessCode, cardsInWallet = userWallet.cardsInWallet) .flatMap { // Save wallet only at first time (SaveWalletBottomSheet). // Otherwise (Example, add new wallet in Details) userWalletsListManager.wallets subscribers will @@ -116,16 +123,14 @@ internal class SaveWalletMiddleware { store.dispatchWithMain(SaveWalletAction.Save.Error(error)) } .doOnSuccess { - store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) - .saveShouldSaveUserWallets(item = true) + store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) // Enable saving access codes only if this is the first time user save the wallet if (isFirstSavedWallet) { preferencesStorage.shouldSaveAccessCodes = true - store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) - .setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = userWallet.hasAccessCode, - ) + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = userWallet.hasAccessCode, + ) } store.dispatchOnMain(SaveWalletAction.Save.Success) @@ -141,6 +146,46 @@ internal class SaveWalletMiddleware { } } + private fun allowToUseBiometrics(state: SaveWalletState) { + val scanResponse = state.backupInfo?.scanResponse + ?: store.state.globalState.scanResponse + ?: return + + if (state.backupInfo != null) { + // TODO: Remove after onboarding refactoring + Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On)) + } else { + Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On)) + } + + scope.launch { + val userWallet = userWalletsListManager.selectedUserWalletSync + ?: UserWalletBuilder(scanResponse) + .backupCardsIds(state.backupInfo?.backupCardsIds) + .build() + ?: return@launch + + store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) + + saveAccessCodeIfNeeded(accessCode = state.backupInfo?.accessCode, cardsInWallet = userWallet.cardsInWallet) + .flatMap { + userWalletsListManager.save(userWallet, canOverride = true) + } + .doOnFailure { error -> + store.dispatchWithMain(SaveWalletAction.Save.Error(error)) + } + .doOnSuccess { + preferencesStorage.shouldSaveAccessCodes = true + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = userWallet.hasAccessCode, + ) + + store.dispatchOnMain(SaveWalletAction.Save.Success) + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + } + } + } + private suspend fun provideLockableUserWalletsListManagerIfNot() { if (store.state.globalState.userWalletsListManager?.isLockable == true) return diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt index f2552462b6..b1c7269494 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt @@ -21,9 +21,9 @@ internal object SaveWalletReducer { backupCardsIds = action.backupCardsIds, ), ) - is SaveWalletAction.Save -> state.copy( - isSaveInProgress = true, - ) + is SaveWalletAction.Save, + is SaveWalletAction.AllowToUseBiometrics, + -> state.copy(isSaveInProgress = true) is SaveWalletAction.Save.Error -> state.copy( error = action.error, isSaveInProgress = false, diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt index 4c56a3b9b4..99947493fc 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.saveWallet.ui import androidx.lifecycle.ViewModel import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.details.ui.cardsettings.TextReference @@ -19,6 +20,7 @@ import javax.inject.Inject @HiltViewModel internal class SaveWalletViewModel @Inject constructor( + private val userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles, private val analyticsEventHandler: AnalyticsEventHandler, ) : ViewModel(), StoreSubscriber { private val stateInternal = MutableStateFlow(SaveWalletScreenState()) @@ -31,7 +33,12 @@ internal class SaveWalletViewModel @Inject constructor( fun saveWallet() { analyticsEventHandler.send(WalletScreenAnalyticsEvent.MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On)) - store.dispatch(SaveWalletAction.Save) + + if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) { + store.dispatch(SaveWalletAction.AllowToUseBiometrics) + } else { + store.dispatch(SaveWalletAction.Save) + } } fun cancelOrClose() { diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 4a543195d6..78bec8cd19 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -12,6 +12,8 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles @@ -55,6 +57,8 @@ data class DaggerGraphState( val currenciesRepository: CurrenciesRepository? = null, val blockchainDataStorage: BlockchainDataStorage? = null, val accountCreator: AccountCreator? = null, + val userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles? = null, + val generalUserWalletsListManager: UserWalletsListManager? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { 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 e76218c369..0180204de2 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -14,5 +14,9 @@ { "name": "REDESIGNED_SEND_SCREEN_ENABLED", "version": "undefined" + }, + { + "name": "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED", + "version": "5.7.0" } ] diff --git a/domain/legacy/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt b/domain/legacy/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt deleted file mode 100644 index 70e3d0ada2..0000000000 --- a/domain/legacy/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.domain.features - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.tangem.feature2.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerFeatureToggles.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerFeatureToggles.kt new file mode 100644 index 0000000000..d5af7e4449 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.wallets.legacy + +interface UserWalletsListManagerFeatureToggles { + + val isGeneralManagerEnabled: Boolean +} \ No newline at end of file From 35898a31cd9b44b45024344c05c83890905fadb0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Feb 2024 19:24:05 +0500 Subject: [PATCH 07/33] Updated on 2026-08-14 --- .../state/SendNotificationFactory.kt | 2 +- .../presentation/state/SendStateFactory.kt | 2 +- .../impl/presentation/state/SendUiState.kt | 8 ++- .../impl/presentation/state/StateRouter.kt | 55 +++++++++++-------- .../state/fee/FeeNotificationFactory.kt | 2 +- .../presentation/ui/SendNavigationButtons.kt | 45 +++++++++------ .../send/impl/presentation/ui/SendScreen.kt | 11 ++-- .../presentation/viewmodel/SendViewModel.kt | 10 ++-- 8 files changed, 82 insertions(+), 53 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt index 05d56ca70b..cdfb8fc271 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt @@ -27,7 +27,7 @@ internal class SendNotificationFactory( ) { fun create(): Flow> = currentStateProvider().currentState - .filter { it == SendUiStateType.Send } + .filter { it.type == SendUiStateType.Send } .map { val state = currentStateProvider() val feeState = state.feeState ?: return@map persistentListOf() 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 57a1a698ae..bbace2d0ac 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 @@ -78,7 +78,7 @@ internal class SendStateFactory( // region UI states fun getInitialState(): SendUiState = SendUiState( clickIntents = clickIntents, - currentState = MutableStateFlow(SendUiStateType.None), + currentState = MutableStateFlow(SendUiCurrentScreen(type = SendUiStateType.None, isFromConfirmation = false)), event = consumedEvent(), isEditingDisabled = false, isBalanceHidden = false, 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 1828a6b3ed..9f1c13016c 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 @@ -33,7 +33,7 @@ internal data class SendUiState( val feeState: SendStates.FeeState? = null, val sendState: SendStates.SendState = SendStates.SendState(), val recipientList: MutableStateFlow> = MutableStateFlow(PagingData.empty()), - val currentState: StateFlow, + val currentState: StateFlow, val isBalanceHidden: Boolean, val event: StateEvent, ) @@ -97,10 +97,16 @@ internal sealed class SendStates { val transactionDate: Long = 0L, val txUrl: String = "", val ignoreAmountReduce: Boolean = false, + val isFromConfirmation: Boolean = true, val notifications: ImmutableList = persistentListOf(), ) : SendStates() } +data class SendUiCurrentScreen( + val type: SendUiStateType, + val isFromConfirmation: Boolean, +) + enum class SendUiStateType { None, Amount, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index ea62b4700d..21a6c3e9db 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -14,58 +14,65 @@ internal class StateRouter( private val analyticsEventsHandler: AnalyticsEventHandler, private val isEditingDisabled: Boolean, ) { - private var mutableCurrentState: MutableStateFlow = MutableStateFlow( + private var mutableCurrentState: MutableStateFlow = MutableStateFlow( if (isEditingDisabled) { - SendUiStateType.None + SendUiCurrentScreen( + type = SendUiStateType.None, + isFromConfirmation = false, + ) } else { - SendUiStateType.Recipient + SendUiCurrentScreen( + type = SendUiStateType.Recipient, + isFromConfirmation = false, + ) }, ) - val currentState: StateFlow = mutableCurrentState + val currentState: StateFlow = mutableCurrentState fun popBackStack() { fragmentManager.get()?.popBackStack() } fun onBackClick(isSuccess: Boolean = false) { + val type = currentState.value.type when { isSuccess -> popBackStack() - isEditingDisabled -> when (currentState.value) { + isEditingDisabled -> when (type) { SendUiStateType.Send -> { analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee)) showFee() } else -> popBackStack() } - else -> when (currentState.value) { + else -> when (type) { SendUiStateType.Amount -> { analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Address)) - showRecipient() + continueToSend(::showRecipient) } SendUiStateType.Fee -> { analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Amount)) - showAmount() + continueToSend(::showAmount) } SendUiStateType.Send -> { analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Fee)) - showFee() + continueToSend(::showFee) } - else -> popBackStack() + else -> continueToSend(::popBackStack) } } } fun onNextClick(): SendUiStateType { - val prevState = currentState.value - when (currentState.value) { + val prevState = currentState.value.type + when (currentState.value.type) { SendUiStateType.Recipient -> { analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Amount)) - showAmount() + continueToSend(::showAmount) } SendUiStateType.Amount -> { analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Fee)) - showFee() + continueToSend(::showFee) } SendUiStateType.Fee -> { analyticsEventsHandler.send(SendAnalyticEvents.NextButtonClicked(SendScreenSource.Fee)) @@ -83,7 +90,7 @@ internal class StateRouter( if (isEditingDisabled) { popBackStack() } else { - when (currentState.value) { + when (currentState.value.type) { SendUiStateType.Amount -> { analyticsEventsHandler.send(SendAnalyticEvents.BackButtonClicked(SendScreenSource.Amount)) showRecipient() @@ -97,23 +104,27 @@ internal class StateRouter( } } - fun showAmount() { + fun showAmount(isFromConfirmation: Boolean = false) { analyticsEventsHandler.send(SendAnalyticEvents.AmountScreenOpened) - mutableCurrentState.update { SendUiStateType.Amount } + mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Amount, isFromConfirmation) } } - fun showRecipient() { + fun showRecipient(isFromConfirmation: Boolean = false) { analyticsEventsHandler.send(SendAnalyticEvents.AddressScreenOpened) - mutableCurrentState.update { SendUiStateType.Recipient } + mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Recipient, isFromConfirmation) } } - fun showFee() { + fun showFee(isFromConfirmation: Boolean = false) { analyticsEventsHandler.send(SendAnalyticEvents.FeeScreenOpened) - mutableCurrentState.update { SendUiStateType.Fee } + mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Fee, isFromConfirmation) } + } + + private fun continueToSend(show: () -> Unit) { + if (currentState.value.isFromConfirmation) showSend() else show() } private fun showSend() { analyticsEventsHandler.send(SendAnalyticEvents.ConfirmationScreenOpened) - mutableCurrentState.update { SendUiStateType.Send } + mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Send, isFromConfirmation = false) } } } \ No newline at end of file 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 fd4fa1da94..3581ba0610 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 @@ -32,7 +32,7 @@ internal class FeeNotificationFactory( ) { fun create() = currentStateProvider().currentState - .filter { it == SendUiStateType.Fee } + .filter { it.type == SendUiStateType.Fee } .map { val state = currentStateProvider() val feeState = state.feeState ?: return@map persistentListOf() 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 b0462ca18f..6fe98a42ac 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 @@ -15,7 +15,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.State -import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -24,25 +23,29 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType @Composable -internal fun SendNavigationButtons(uiState: SendUiState) { +internal fun SendNavigationButtons(uiState: SendUiState, currentState: State) { Row( modifier = Modifier .fillMaxWidth() .padding(bottom = TangemTheme.dimens.spacing12), ) { - SendSecondaryNavigationButton(uiState) + SendSecondaryNavigationButton( + uiState = uiState, + currentState = currentState, + ) SendPrimaryNavigationButton( uiState = uiState, + currentState = currentState, modifier = Modifier .weight(1f) .padding(horizontal = TangemTheme.dimens.spacing16), @@ -51,12 +54,13 @@ internal fun SendNavigationButtons(uiState: SendUiState) { } @Composable -private fun SendSecondaryNavigationButton(uiState: SendUiState) { - val currentState = uiState.currentState.collectAsState() +private fun SendSecondaryNavigationButton(uiState: SendUiState, currentState: State) { val isEditingDisabled = uiState.isEditingDisabled - val isCorrectScreen = currentState.value == SendUiStateType.Amount || currentState.value == SendUiStateType.Fee + val isFromConfirmation = currentState.value.isFromConfirmation + val isCorrectScreen = + currentState.value.type == SendUiStateType.Amount || currentState.value.type == SendUiStateType.Fee AnimatedVisibility( - visible = !isEditingDisabled && isCorrectScreen, + visible = !isEditingDisabled && isCorrectScreen && !isFromConfirmation, enter = expandHorizontally(expandFrom = Alignment.End), exit = shrinkHorizontally(shrinkTowards = Alignment.End), ) { @@ -77,8 +81,11 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState) { } @Composable -private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) { - val currentState = uiState.currentState.collectAsStateWithLifecycle() +private fun SendPrimaryNavigationButton( + uiState: SendUiState, + currentState: State, + modifier: Modifier = Modifier, +) { val isSuccess = uiState.sendState.isSuccess val isSending = uiState.sendState.isSending val txUrl = uiState.sendState.txUrl @@ -100,7 +107,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier modifier = modifier, ) { textId -> when { - currentState.value == SendUiStateType.Send && !isSuccess -> { + currentState.value.type == SendUiStateType.Send && !isSuccess -> { val hapticFeedback = rememberHapticFeedback(state = currentState, onAction = buttonClick) PrimaryButtonIconEnd( text = stringResource(textId), @@ -110,7 +117,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier showProgress = isSending, ) } - currentState.value == SendUiStateType.Send && isSuccess -> { + currentState.value.type == SendUiStateType.Send && isSuccess -> { PrimaryButtonsDone( textRes = textId, txUrl = txUrl, @@ -177,15 +184,19 @@ private fun PrimaryButtonsDone( private fun getButtonData( uiState: SendUiState, - currentState: State, + currentState: State, isSuccess: Boolean, ): Pair Unit> { - return when (currentState.value) { + return when (currentState.value.type) { SendUiStateType.None, SendUiStateType.Amount, SendUiStateType.Recipient, SendUiStateType.Fee, - -> R.string.common_next to uiState.clickIntents::onNextClick + -> if (currentState.value.isFromConfirmation) { + R.string.common_continue to uiState.clickIntents::onNextClick + } else { + R.string.common_next to uiState.clickIntents::onNextClick + } SendUiStateType.Send -> if (isSuccess) { R.string.common_close } else { @@ -194,8 +205,8 @@ private fun getButtonData( } } -private fun isButtonEnabled(currentState: State, uiState: SendUiState): Boolean { - return when (currentState.value) { +private fun isButtonEnabled(currentState: State, uiState: SendUiState): Boolean { + return when (currentState.value.type) { SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled ?: false SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled ?: false 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 88c36f0d4c..6e862d9e07 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 @@ -19,6 +19,7 @@ import androidx.paging.compose.collectAsLazyPagingItems import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen 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.ui.amount.SendAmountContent @@ -40,14 +41,14 @@ internal fun SendScreen(uiState: SendUiState) { .background(color = TangemTheme.colors.background.tertiary), horizontalAlignment = Alignment.CenterHorizontally, ) { - val titleRes = when (currentState.value) { + val titleRes = when (currentState.value.type) { SendUiStateType.Amount -> R.string.send_amount_label SendUiStateType.Recipient -> R.string.send_recipient_label SendUiStateType.Fee -> R.string.common_fee_selector_title SendUiStateType.Send -> if (!isSuccess) R.string.send_confirm_label else null else -> null } - val iconRes = if (currentState.value == SendUiStateType.Recipient) { + val iconRes = if (currentState.value.type == SendUiStateType.Recipient) { R.drawable.ic_qrcode_scan_24 } else { null @@ -67,7 +68,7 @@ internal fun SendScreen(uiState: SendUiState) { modifier = Modifier .weight(1f), ) - SendNavigationButtons(uiState) + SendNavigationButtons(uiState, currentState) } SendEventEffect( @@ -79,7 +80,7 @@ internal fun SendScreen(uiState: SendUiState) { @Composable private fun SendScreenContent( uiState: SendUiState, - currentState: State, + currentState: State, modifier: Modifier = Modifier, ) { val recipientList = uiState.recipientList.collectAsLazyPagingItems() @@ -88,7 +89,7 @@ private fun SendScreenContent( label = "Send Scree Navigation", modifier = modifier, ) { state -> - when (state) { + when (state.type) { SendUiStateType.Amount -> SendAmountContent( amountState = uiState.amountState, isBalanceHiding = uiState.isBalanceHidden, 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 c202ee727b..b1da2b4460 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 @@ -377,8 +377,8 @@ internal class SendViewModel @Inject constructor( private fun onStateActive() { uiState.currentState .onEach { - when (it) { - SendUiStateType.Fee -> loadFee() + when (it.type) { + SendUiStateType.Fee -> if (!it.isFromConfirmation) loadFee() SendUiStateType.Send -> sendIdleTimer = System.currentTimeMillis() else -> Unit } @@ -576,17 +576,17 @@ internal class SendViewModel @Inject constructor( } override fun showAmount() { - stateRouter.showAmount() + stateRouter.showAmount(isFromConfirmation = true) analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount)) } override fun showRecipient() { - stateRouter.showRecipient() + stateRouter.showRecipient(isFromConfirmation = true) analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address)) } override fun showFee() { - stateRouter.showFee() + stateRouter.showFee(isFromConfirmation = true) analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) } From cf5787d252af2d9167f4bfe8260f665e03238eb3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Feb 2024 13:03:56 +0500 Subject: [PATCH 08/33] Updated on 2026-08-14 --- .../presentation/ui/fee/SendSpeedSelector.kt | 45 ++++++++++++++++--- .../viewmodel/SendClickIntents.kt | 2 + .../presentation/viewmodel/SendViewModel.kt | 16 +++++++ 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index 0bac355bb7..611c29dd70 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -10,18 +10,23 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.ClickableText import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +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.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee @@ -43,7 +48,6 @@ 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.fee.SendFeeNotification -import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import java.math.BigDecimal @@ -60,10 +64,7 @@ internal fun SendSpeedSelector( clickIntents: SendClickIntents, modifier: Modifier = Modifier, ) { - FooterContainer( - footer = stringResource(R.string.common_fee_selector_footer), - modifier = modifier, - ) { + Column(modifier = modifier) { Column( modifier = Modifier .fillMaxWidth() @@ -147,9 +148,43 @@ internal fun SendSpeedSelector( } } } + FooterText(clickIntents::onReadMoreClick) } } +@Composable +private fun FooterText(onReadMoreClick: () -> Unit) { + val linkText = stringResource(R.string.common_fee_selector_link_description) + val fullString = stringResource(R.string.common_fee_selector_footer, linkText) + val linkTextPosition = fullString.length - linkText.length + val defaultStyle = TangemTheme.colors.text.tertiary + val linkStyle = TangemTheme.colors.text.accent + val annotatedString = remember(defaultStyle, linkStyle) { + buildAnnotatedString { + withStyle(SpanStyle(defaultStyle)) { + append(fullString.substring(0, linkTextPosition)) + } + withStyle(SpanStyle(linkStyle)) { + append(fullString.substring(linkTextPosition, fullString.length)) + } + } + } + + val click = { i: Int -> + val readMoreStyle = requireNotNull(annotatedString.spanStyles.getOrNull(1)) + if (i in readMoreStyle.start..readMoreStyle.end) { + onReadMoreClick() + } + } + + ClickableText( + text = annotatedString, + style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Start), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + onClick = click, + ) +} + // todo remove after refactoring [REDACTED_JIRA] private fun getCryptoReference(amount: Amount, isFeeApproximate: Boolean) = combinedReference( if (isFeeApproximate) stringReference("$CAN_BE_LOWER_SIGN ") else TextReference.EMPTY, 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 18252b0ed4..8d409188fd 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 @@ -44,6 +44,8 @@ internal interface SendClickIntents { fun onCustomFeeValueChange(index: Int, value: String) fun onSubtractSelect(value: Boolean) + + fun onReadMoreClick() // endregion // region Send 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 c202ee727b..3cbab750e5 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 @@ -57,6 +57,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber +import java.util.Locale import javax.inject.Inject import kotlin.properties.Delegates @@ -524,6 +525,16 @@ internal class SendViewModel @Inject constructor( updateFeeNotifications() } + override fun onReadMoreClick() { + val locale = if (Locale.getDefault().language == RU_LOCALE) RU_LOCALE else EN_LOCALE + val url = buildString { + append(FEE_READ_MORE_URL_FIRST_PART) + append(locale) + append(FEE_READ_MORE_URL_SECOND_PART) + } + innerRouter.openUrl(url) + } + private fun loadFee() { viewModelScope.launch(dispatchers.main) { uiState = feeStateFactory.onFeeOnLoadingState() @@ -723,5 +734,10 @@ internal class SendViewModel @Inject constructor( companion object { private const val CHECK_FEE_UPDATE_DELAY = 60_000L private 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/" } } \ No newline at end of file From a2f57e10befda10e9099ce70add78d54ef1a86d5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Feb 2024 14:28:14 +0700 Subject: [PATCH 09/33] Updated on 2026-08-14 --- app/build.gradle.kts | 2 ++ .../java/com/tangem/tap/TapApplication.kt | 10 ++++++ .../tap/di/domain/OnboardingDomainModule.kt | 31 +++++++++++++++++ .../domain/scanCard/LegacyScanProcessor.kt | 15 +++++---- .../domain/scanCard/UseCaseScanProcessor.kt | 3 +- .../chains/CheckForOnboardingChain.kt | 11 ++++--- .../twins/redux/TwinCardsMiddleware.kt | 32 +++++++++++------- .../tap/proxy/redux/DaggerGraphState.kt | 4 +++ .../local/preferences/PreferencesKeys.kt | 4 +++ data/onboarding/.gitignore | 1 + data/onboarding/build.gradle.kts | 30 +++++++++++++++++ .../onboarding/DefaultOnboardingRepository.kt | 33 +++++++++++++++++++ .../di/OnboardingRepositoryModule.kt | 21 ++++++++++++ .../preferences/PreferencesDataSource.kt | 9 ----- domain/onboarding/.gitignore | 1 + domain/onboarding/build.gradle.kts | 9 +++++ .../SaveTwinsOnboardingShownUseCase.kt | 19 +++++++++++ .../WasTwinsOnboardingShownUseCase.kt | 33 +++++++++++++++++++ .../repository/OnboardingRepository.kt | 18 ++++++++++ .../extension/BaseExtensionConfigurations.kt | 2 +- settings.gradle.kts | 2 ++ 21 files changed, 257 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/OnboardingDomainModule.kt create mode 100644 data/onboarding/.gitignore create mode 100644 data/onboarding/build.gradle.kts create mode 100644 data/onboarding/src/main/kotlin/com/tangem/data/onboarding/DefaultOnboardingRepository.kt create mode 100644 data/onboarding/src/main/kotlin/com/tangem/data/onboarding/di/OnboardingRepositoryModule.kt create mode 100644 domain/onboarding/.gitignore create mode 100644 domain/onboarding/build.gradle.kts create mode 100644 domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/SaveTwinsOnboardingShownUseCase.kt create mode 100644 domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/WasTwinsOnboardingShownUseCase.kt create mode 100644 domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/repository/OnboardingRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4d977ecce4..553088615c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -60,6 +60,7 @@ dependencies { implementation(projects.domain.transaction) implementation(projects.domain.analytics) implementation(projects.domain.visa) + implementation(projects.domain.onboarding) implementation(projects.common) implementation(projects.core.analytics) @@ -88,6 +89,7 @@ dependencies { implementation(projects.data.transaction) implementation(projects.data.visa) implementation(projects.data.promo) + implementation(projects.data.onboarding) /** Features */ implementation(projects.features.onboarding) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 5ca956a2bf..83452572ae 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -31,6 +31,8 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.LogConfig +import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase +import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -162,6 +164,12 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var generalUserWalletsListManager: UserWalletsListManager + + @Inject + lateinit var wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase + + @Inject + lateinit var saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase // endregion Injected override fun onCreate() { @@ -246,6 +254,8 @@ internal class TapApplication : Application(), ImageLoaderFactory { accountCreator = accountCreator, userWalletsListManagerFeatureToggles = userWalletsListManagerFeatureToggles, generalUserWalletsListManager = generalUserWalletsListManager, + wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase, + saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnboardingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnboardingDomainModule.kt new file mode 100644 index 0000000000..2ec4dabe50 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/OnboardingDomainModule.kt @@ -0,0 +1,31 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase +import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase +import com.tangem.domain.onboarding.repository.OnboardingRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object OnboardingDomainModule { + + @Provides + @Singleton + fun provideWasTwinsOnboardingShownUseCase( + onboardingRepository: OnboardingRepository, + ): WasTwinsOnboardingShownUseCase { + return WasTwinsOnboardingShownUseCase(onboardingRepository) + } + + @Provides + @Singleton + fun provideSaveTwinsOnboardingShownUseCase( + onboardingRepository: OnboardingRepository, + ): SaveTwinsOnboardingShownUseCase { + return SaveTwinsOnboardingShownUseCase(onboardingRepository) + } +} \ No newline at end of file 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 a1e3f77974..757e78cb34 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 @@ -13,12 +13,8 @@ import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.* import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor -import com.tangem.tap.common.extensions.addContext -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.setContext +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction @@ -26,6 +22,10 @@ import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -165,7 +165,10 @@ internal object LegacyScanProcessor { navigateTo(appScreen) { onProgressStateChange(it) } } else { Analytics.setContext(scanResponse) - if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) { + + val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() + + if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { onWalletNotCreated() store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse))) navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) } 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 562ac25718..b5949c7c5a 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 @@ -11,7 +11,6 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.domain.scanCard.chains.* import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter -import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE @@ -52,7 +51,7 @@ internal object UseCaseScanProcessor { add(AnalyticsChain(analyticsEvent)) } add(DisclaimerChain(store, disclaimerWillShow)) - add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager, preferencesStorage)) + add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager)) } scanCardUseCase(cardId, afterScanChains = chains) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index cb6471a4ca..3db1f7a79e 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -5,7 +5,6 @@ import arrow.core.left import arrow.core.right import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen -import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.domain.card.ScanCardException import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.util.twinsIsTwinned @@ -13,6 +12,7 @@ import com.tangem.domain.core.chain.Chain import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.addContext import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -20,6 +20,7 @@ import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay import org.rekotlin.Store @@ -32,14 +33,12 @@ import org.rekotlin.Store * * @param store the [Store] that holds the state of the app. * @param tapWalletManager manager responsible for handling operations related to the Wallet. - * @param preferencesStore data source for user preferences. * * @see Chain for more information about the Chain interface. */ class CheckForOnboardingChain( private val store: Store, private val tapWalletManager: TapWalletManager, - private val preferencesStore: PreferencesDataSource, ) : Chain { override suspend fun invoke( @@ -63,8 +62,12 @@ class CheckForOnboardingChain( } else -> { Analytics.setContext(previousChainResult) + + val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) + .invokeSync() + // If twins was twinned previously but twins welcome not shown - if (previousChainResult.twinsIsTwinned() && !preferencesStore.wasTwinsOnboardingShown()) { + if (previousChainResult.twinsIsTwinned() && !wasTwinsOnboardingShown) { store.dispatchOnMain( TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)), ) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index fafb9a473f..4a7906dfdb 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -26,7 +26,7 @@ import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper -import com.tangem.tap.preferencesStorage +import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store @@ -100,15 +100,22 @@ private fun handle(action: Action, dispatch: DispatchFunction) { when (twinCardsState.mode) { CreateTwinWalletMode.CreateWallet -> { - if (preferencesStorage.wasTwinsOnboardingShown()) { - val step = when { - !scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet - twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done - else -> TwinCardsStep.TopUpWallet + mainScope.launch { + val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) + .invokeSync() + + val dispatchAction = if (wasTwinsOnboardingShown) { + val step = when { + !scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet + twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done + else -> TwinCardsStep.TopUpWallet + } + TwinCardsAction.SetStepOfScreen(step) + } else { + TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome) } - store.dispatch(TwinCardsAction.SetStepOfScreen(step)) - } else { - store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome)) + + store.dispatch(dispatchAction) } } CreateTwinWalletMode.RecreateWallet -> { @@ -120,7 +127,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) { when (action.step) { is TwinCardsStep.WelcomeOnly, TwinCardsStep.Welcome -> { Analytics.send(Onboarding.Twins.ScreenOpened()) - preferencesStorage.saveTwinsOnboardingShown() + + scope.launch { + store.inject(DaggerGraphState::saveTwinsOnboardingShownUseCase).invoke() + } } is TwinCardsStep.CreateFirstWallet -> { Analytics.send(Onboarding.CreateWallet.ScreenOpened()) @@ -295,7 +305,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } CreateTwinWalletMode.RecreateWallet -> { scope.launch { - val walletsRepository = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + val walletsRepository = store.inject(DaggerGraphState::walletsRepository) if (walletsRepository.shouldSaveUserWalletsSync()) { OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 78bec8cd19..e552d90fd6 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -9,6 +9,8 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase +import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -59,6 +61,8 @@ data class DaggerGraphState( val accountCreator: AccountCreator? = null, val userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles? = null, val generalUserWalletsListManager: UserWalletsListManager? = null, + val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null, + val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { 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 c5c54133ac..2b74eabdad 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 @@ -7,6 +7,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN /** * All preferences keys that DataStore is stored. @@ -54,6 +55,8 @@ object PreferencesKeys { } val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") } + + val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ @@ -65,6 +68,7 @@ internal fun getTapPrefKeysToMigrate(): Set { FUNDS_FOUND_DATE_KEY, USER_WAS_INTERACT_WITH_RATING_KEY, USED_CARDS_INFO_KEY, + WAS_TWINS_ONBOARDING_SHOWN, ) .map(Preferences.Key<*>::name) .toSet() diff --git a/data/onboarding/.gitignore b/data/onboarding/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/onboarding/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/onboarding/build.gradle.kts b/data/onboarding/build.gradle.kts new file mode 100644 index 0000000000..91e15d90c9 --- /dev/null +++ b/data/onboarding/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.onboarding" +} + +dependencies { + + // region AndroidX libraries + implementation(deps.androidx.datastore) + // endregion + + // region DI + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + // endregion + + // region Core modules + implementation(projects.core.datasource) + // endregion + + // region Domain modules + implementation(projects.domain.onboarding) + // endregion +} \ No newline at end of file diff --git a/data/onboarding/src/main/kotlin/com/tangem/data/onboarding/DefaultOnboardingRepository.kt b/data/onboarding/src/main/kotlin/com/tangem/data/onboarding/DefaultOnboardingRepository.kt new file mode 100644 index 0000000000..683ce6d376 --- /dev/null +++ b/data/onboarding/src/main/kotlin/com/tangem/data/onboarding/DefaultOnboardingRepository.kt @@ -0,0 +1,33 @@ +package com.tangem.data.onboarding + +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.get +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store +import com.tangem.domain.onboarding.repository.OnboardingRepository +import kotlinx.coroutines.flow.Flow + +/** + * Default implementation of [OnboardingRepository] + * + * @property appPreferencesStore app preferences store + * +[REDACTED_AUTHOR] + */ +internal class DefaultOnboardingRepository( + private val appPreferencesStore: AppPreferencesStore, +) : OnboardingRepository { + + override fun wasTwinsOnboardingShown(): Flow { + return appPreferencesStore.get(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, default = false) + } + + override suspend fun wasTwinsOnboardingShownSync(): Boolean { + return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, default = false) + } + + override suspend fun saveTwinsOnboardingShown() { + appPreferencesStore.store(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, value = true) + } +} \ No newline at end of file diff --git a/data/onboarding/src/main/kotlin/com/tangem/data/onboarding/di/OnboardingRepositoryModule.kt b/data/onboarding/src/main/kotlin/com/tangem/data/onboarding/di/OnboardingRepositoryModule.kt new file mode 100644 index 0000000000..391bade42f --- /dev/null +++ b/data/onboarding/src/main/kotlin/com/tangem/data/onboarding/di/OnboardingRepositoryModule.kt @@ -0,0 +1,21 @@ +package com.tangem.data.onboarding.di + +import com.tangem.data.onboarding.DefaultOnboardingRepository +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.onboarding.repository.OnboardingRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object OnboardingRepositoryModule { + + @Provides + @Singleton + fun provideOnboardingRepository(appPreferencesStore: AppPreferencesStore): OnboardingRepository { + return DefaultOnboardingRepository(appPreferencesStore = appPreferencesStore) + } +} \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt index d16da10d47..91f774282a 100644 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt +++ b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt @@ -56,14 +56,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con putBoolean(OPEN_WELCOME_ON_RESUME_KEY, value) } - fun saveTwinsOnboardingShown() { - preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) } - } - - fun wasTwinsOnboardingShown(): Boolean { - return preferences.getBoolean(TWINS_ONBOARDING_SHOWN_KEY, false) - } - private fun incrementLaunchCounter() { var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0) preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) } @@ -71,7 +63,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con companion object { private const val PREFERENCES_NAME = "tapPrefs" - private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown" private const val APP_LAUNCH_COUNT_KEY = "launchCount" private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown" private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes" diff --git a/domain/onboarding/.gitignore b/domain/onboarding/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/onboarding/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/onboarding/build.gradle.kts b/domain/onboarding/build.gradle.kts new file mode 100644 index 0000000000..831b4eb4e4 --- /dev/null +++ b/domain/onboarding/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) +} \ No newline at end of file diff --git a/domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/SaveTwinsOnboardingShownUseCase.kt b/domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/SaveTwinsOnboardingShownUseCase.kt new file mode 100644 index 0000000000..fb6bf89e87 --- /dev/null +++ b/domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/SaveTwinsOnboardingShownUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.onboarding + +import com.tangem.domain.onboarding.repository.OnboardingRepository + +/** + * Saves that twins onboarding was shown + * + * @property onboardingRepository onboarding repository + * +[REDACTED_AUTHOR] + */ +class SaveTwinsOnboardingShownUseCase( + private val onboardingRepository: OnboardingRepository, +) { + + suspend operator fun invoke() { + onboardingRepository.saveTwinsOnboardingShown() + } +} \ No newline at end of file diff --git a/domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/WasTwinsOnboardingShownUseCase.kt b/domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/WasTwinsOnboardingShownUseCase.kt new file mode 100644 index 0000000000..9666714871 --- /dev/null +++ b/domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/WasTwinsOnboardingShownUseCase.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.onboarding + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.onboarding.repository.OnboardingRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +/** + * Use case for checking if twins onboarding was shown + * + * @property onboardingRepository onboarding repository + * +[REDACTED_AUTHOR] + */ +class WasTwinsOnboardingShownUseCase( + private val onboardingRepository: OnboardingRepository, +) { + + /** Get flow with twins onboarding state or error [Throwable] */ + operator fun invoke(): Flow> { + return onboardingRepository.wasTwinsOnboardingShown() + .map(Boolean::right) + .catch { it.left() } + } + + /** Get twins onboarding state synchronously or default value if exception will be thrown */ + suspend fun invokeSync(): Boolean { + return runCatching { onboardingRepository.wasTwinsOnboardingShownSync() }.getOrDefault(defaultValue = false) + } +} \ No newline at end of file diff --git a/domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/repository/OnboardingRepository.kt b/domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/repository/OnboardingRepository.kt new file mode 100644 index 0000000000..cda4f3b19a --- /dev/null +++ b/domain/onboarding/src/main/kotlin/com/tangem/domain/onboarding/repository/OnboardingRepository.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.onboarding.repository + +import kotlinx.coroutines.flow.Flow + +/** + * Onboarding repository + * +[REDACTED_AUTHOR] + */ +interface OnboardingRepository { + + fun wasTwinsOnboardingShown(): Flow + + @Throws + suspend fun wasTwinsOnboardingShownSync(): Boolean + + suspend fun saveTwinsOnboardingShown() +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index 142649ae2a..c47360c0fc 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -20,7 +20,7 @@ internal fun BaseExtension.configureCompilerOptions() { internal fun BaseExtension.configureCompose(project: Project) { val useCompose = with(project.path) { contains(":ui") || - contains(Regex(pattern = ":onboarding\$")) || // TODO: divide on api/impl after migrating all onboarding to module + contains(":features:onboarding") || // TODO: divide on api/impl after migrating all onboarding to module contains(Regex(pattern = ":presentation\$")) || contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function diff --git a/settings.gradle.kts b/settings.gradle.kts index 19fcb80b60..fb31682f87 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -137,6 +137,7 @@ include(":domain:balance-hiding:models") include(":domain:transaction") include(":domain:analytics") include(":domain:visa") +include(":domain:onboarding") // endregion Domain modules // region Data modules @@ -154,4 +155,5 @@ include(":data:analytics") include(":data:transaction") include(":data:visa") include(":data:promo") +include(":data:onboarding") // endregion Data modules \ No newline at end of file From 6df93c1d67a08b8ce0ec5cae56e4f76e381074b2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Feb 2024 14:26:26 +0700 Subject: [PATCH 10/33] Updated on 2026-08-14 --- .../wallet/ui/WalletsListEffects.kt | 7 +- .../ui/components/TangemSnapFlingBehavior.kt | 405 ++++++++++++++++++ .../TangemSnapLayoutInfoProvider.kt | 177 ++++++++ .../wallet/ui/components/WalletsList.kt | 15 +- .../wallet/ui/utils/ScrollOffsetCollector.kt | 15 +- 5 files changed, 607 insertions(+), 12 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TangemSnapFlingBehavior.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TangemSnapLayoutInfoProvider.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffects.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffects.kt index 5765897931..d2173369f4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffects.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffects.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.snapshotFlow +import com.tangem.feature.wallet.presentation.wallet.ui.utils.LazyListItemData import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector import com.tangem.feature.wallet.presentation.wallet.ui.utils.WalletsListInteractionsCollector @@ -19,7 +20,11 @@ internal fun WalletsListEffects( onAutoScrollReset: () -> Unit, ) { LaunchedEffect(key1 = lazyListState, key2 = onWalletChange) { - snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo } + snapshotFlow { + lazyListState.layoutInfo.visibleItemsInfo.map { + LazyListItemData(it.index, it.size, it.offset) + } + } .collect( collector = ScrollOffsetCollector( selectedWalletIndex = selectedWalletIndex, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TangemSnapFlingBehavior.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TangemSnapFlingBehavior.kt new file mode 100644 index 0000000000..4ce45d17f4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TangemSnapFlingBehavior.kt @@ -0,0 +1,405 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import androidx.compose.animation.core.* +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.ui.MotionDurationScale +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.withContext +import kotlin.math.abs +import kotlin.math.absoluteValue +import kotlin.math.sign + +@ExperimentalFoundationApi +class TangemSnapFlingBehavior( + private val snapLayoutInfoProvider: SnapLayoutInfoProvider, + private val lowVelocityAnimationSpec: AnimationSpec, + private val highVelocityAnimationSpec: DecayAnimationSpec, + private val snapAnimationSpec: AnimationSpec, + private val density: Density, + private val shortSnapVelocityThreshold: Dp = MinFlingVelocityDp, +) : FlingBehavior { + + private val velocityThreshold = with(density) { shortSnapVelocityThreshold.toPx() } + private var motionScaleDuration = DefaultScrollMotionDurationScale + + override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { + return performFling(initialVelocity) {} + } + + /** + * Perform a snapping fling animation with given velocity and suspend until fling has + * finished. This will behave the same way as [performFling] except it will report on + * each remainingOffsetUpdate using the [onSettlingDistanceUpdated] lambda. + * + * @param initialVelocity velocity available for fling in the orientation specified in + * [androidx.compose.foundation.gestures.scrollable] that invoked this method. + * + * @param onSettlingDistanceUpdated a lambda that will be called anytime the + * distance to the settling offset is updated. The settling offset is the final offset where + * this fling will stop and may change depending on the snapping animation progression. + * + * @return remaining velocity after fling operation has ended + */ + private suspend fun ScrollScope.performFling( + initialVelocity: Float, + onSettlingDistanceUpdated: (Float) -> Unit, + ): Float { + val (remainingOffset, remainingState) = fling(initialVelocity, onSettlingDistanceUpdated) + + // No remaining offset means we've used everything, no need to propagate velocity. Otherwise + // we couldn't use everything (probably because we have hit the min/max bounds of the + // containing layout) we should propagate the offset. + return if (remainingOffset == 0f) NoVelocity else remainingState.velocity + } + + private suspend fun ScrollScope.fling( + initialVelocity: Float, + onRemainingScrollOffsetUpdate: (Float) -> Unit, + ): AnimationResult { + // If snapping from scroll (short snap) or fling (long snap) + val result = withContext(motionScaleDuration) { + if (abs(initialVelocity) <= abs(velocityThreshold)) { + shortSnap(initialVelocity, onRemainingScrollOffsetUpdate) + } else { + longSnap(initialVelocity, onRemainingScrollOffsetUpdate) + } + } + + onRemainingScrollOffsetUpdate(0f) // Animation finished or was cancelled + return result + } + + private suspend fun ScrollScope.shortSnap( + velocity: Float, + onRemainingScrollOffsetUpdate: (Float) -> Unit, + ): AnimationResult { + val closestOffset = with(snapLayoutInfoProvider) { + density.calculateSnappingOffset(0f) + } + + var remainingScrollOffset = closestOffset + + val animationState = AnimationState(NoDistance, velocity) + return animateSnap( + closestOffset, + closestOffset, + animationState, + snapAnimationSpec, + ) { delta -> + remainingScrollOffset -= delta + onRemainingScrollOffsetUpdate(remainingScrollOffset) + } + } + + private suspend fun ScrollScope.longSnap( + initialVelocity: Float, + onAnimationStep: (remainingScrollOffset: Float) -> Unit, + ): AnimationResult { + val initialOffset = + with(snapLayoutInfoProvider) { density.calculateApproachOffset(initialVelocity) }.let { + abs(it) * sign(initialVelocity) // ensure offset sign is correct + } + var remainingScrollOffset = initialOffset + + onAnimationStep(remainingScrollOffset) // First Scroll Offset + + val (remainingOffset, animationState) = runApproach( + initialOffset, + initialVelocity, + ) { delta -> + remainingScrollOffset -= delta + onAnimationStep(remainingScrollOffset) + } + + remainingScrollOffset = remainingOffset + + return animateSnap( + remainingOffset, + remainingOffset, + animationState.copy(value = 0f), + snapAnimationSpec, + ) { delta -> + remainingScrollOffset -= delta + onAnimationStep(remainingScrollOffset) + } + } + + private suspend fun ScrollScope.runApproach( + initialTargetOffset: Float, + initialVelocity: Float, + onAnimationStep: (delta: Float) -> Unit, + ): AnimationResult { + val animation = + if (isDecayApproachPossible(offset = initialTargetOffset, velocity = initialVelocity)) { + HighVelocityApproachAnimation(highVelocityAnimationSpec) + } else { + LowVelocityApproachAnimation( + lowVelocityAnimationSpec, + snapLayoutInfoProvider, + density, + ) + } + + return approach( + initialTargetOffset, + initialVelocity, + animation, + snapLayoutInfoProvider, + density, + onAnimationStep, + ) + } + + /** + * If we can approach the target and still have velocity left + */ + private fun isDecayApproachPossible(offset: Float, velocity: Float): Boolean { + val decayOffset = highVelocityAnimationSpec.calculateTargetValue(NoDistance, velocity) + val snapStepSize = with(snapLayoutInfoProvider) { density.calculateSnapStepSize() } + return decayOffset.absoluteValue >= offset.absoluteValue + snapStepSize + } + + override fun equals(other: Any?): Boolean { + return if (other is TangemSnapFlingBehavior) { + other.snapAnimationSpec == this.snapAnimationSpec && + other.highVelocityAnimationSpec == this.highVelocityAnimationSpec && + other.lowVelocityAnimationSpec == this.lowVelocityAnimationSpec && + other.snapLayoutInfoProvider == this.snapLayoutInfoProvider && + other.density == this.density && + other.shortSnapVelocityThreshold == this.shortSnapVelocityThreshold + } else { + false + } + } + + override fun hashCode(): Int = 0 + .let { 31 * it + snapAnimationSpec.hashCode() } + .let { 31 * it + highVelocityAnimationSpec.hashCode() } + .let { 31 * it + lowVelocityAnimationSpec.hashCode() } + .let { 31 * it + snapLayoutInfoProvider.hashCode() } + .let { 31 * it + density.hashCode() } + .let { 31 * it + shortSnapVelocityThreshold.hashCode() } +} + +@Suppress("LongParameterList") +@OptIn(ExperimentalFoundationApi::class) +private suspend fun ScrollScope.approach( + initialTargetOffset: Float, + initialVelocity: Float, + animation: ApproachAnimation, + snapLayoutInfoProvider: SnapLayoutInfoProvider, + density: Density, + onAnimationStep: (delta: Float) -> Unit, +): AnimationResult { + val (_, currentAnimationState) = animation.approachAnimation( + this, + initialTargetOffset, + initialVelocity, + onAnimationStep, + ) + + val remainingOffset = with(snapLayoutInfoProvider) { + density.calculateSnappingOffset(currentAnimationState.velocity) + } + + // will snap the remainder + return AnimationResult(remainingOffset, currentAnimationState) +} + +/** + * Runs a [AnimationSpec] to snap the list into [targetOffset]. Uses [cancelOffset] to stop this + * animation before it reaches the target. + * + * @param targetOffset The final target of this animation + * @param cancelOffset If we'd like to finish the animation earlier we use this value + * @param animationState The current animation state for continuation purposes + * @param snapAnimationSpec The [AnimationSpec] that will drive this animation + * @param onAnimationStep Called for each new scroll delta emitted by the animation cycle. + */ +@Suppress("MagicNumber") +private suspend fun ScrollScope.animateSnap( + targetOffset: Float, + cancelOffset: Float, + animationState: AnimationState, + snapAnimationSpec: AnimationSpec, + onAnimationStep: (delta: Float) -> Unit, +): AnimationResult { + var consumedUpToNow = 0f + val initialVelocity = animationState.velocity + animationState.animateTo( + targetOffset, + animationSpec = snapAnimationSpec, + sequentialAnimation = animationState.velocity != 0f, + ) { + val realValue = value.coerceToTarget(cancelOffset) + val delta = realValue - consumedUpToNow + val consumed = scrollBy(delta) + onAnimationStep(consumed) + // stop when unconsumed or when we reach the desired value + if (abs(delta - consumed) > 0.5f || realValue != value) { + cancelAnimation() + } + consumedUpToNow += consumed + } + + // Always course correct velocity so they don't become too large. + val finalVelocity = animationState.velocity.coerceToTarget(initialVelocity) + return AnimationResult( + targetOffset - consumedUpToNow, + animationState.copy(velocity = finalVelocity), + ) +} + +private class HighVelocityApproachAnimation( + private val decayAnimationSpec: DecayAnimationSpec, +) : ApproachAnimation { + override suspend fun approachAnimation( + scope: ScrollScope, + offset: Float, + velocity: Float, + onAnimationStep: (delta: Float) -> Unit, + ): AnimationResult { + val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity) + return with(scope) { + animateDecay(offset, animationState, decayAnimationSpec, onAnimationStep) + } + } +} + +private class LowVelocityApproachAnimation @OptIn(ExperimentalFoundationApi::class) constructor( + private val lowVelocityAnimationSpec: AnimationSpec, + private val layoutInfoProvider: SnapLayoutInfoProvider, + private val density: Density, +) : ApproachAnimation { + @OptIn(ExperimentalFoundationApi::class) + override suspend fun approachAnimation( + scope: ScrollScope, + offset: Float, + velocity: Float, + onAnimationStep: (delta: Float) -> Unit, + ): AnimationResult { + val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity) + val targetOffset = + (abs(offset) + with(layoutInfoProvider) { density.calculateSnapStepSize() }) * sign( + velocity, + ) + return with(scope) { + animateSnap( + targetOffset = targetOffset, + cancelOffset = offset, + animationState = animationState, + snapAnimationSpec = lowVelocityAnimationSpec, + onAnimationStep = onAnimationStep, + ) + } + } +} + +@Suppress("MagicNumber") +private suspend fun ScrollScope.animateDecay( + targetOffset: Float, + animationState: AnimationState, + decayAnimationSpec: DecayAnimationSpec, + onAnimationStep: (delta: Float) -> Unit, +): AnimationResult { + var previousValue = 0f + + fun AnimationScope.consumeDelta(delta: Float) { + val consumed = scrollBy(delta) + onAnimationStep(consumed) + if (abs(delta - consumed) > 0.5f) cancelAnimation() + } + + animationState.animateDecay( + animationSpec = decayAnimationSpec, + sequentialAnimation = animationState.velocity != 0f, + ) { + previousValue = if (abs(value) >= abs(targetOffset)) { + val finalValue = value.coerceToTarget(targetOffset) + val finalDelta = finalValue - previousValue + consumeDelta(finalDelta) + cancelAnimation() + finalValue + } else { + val delta = value - previousValue + consumeDelta(delta) + value + } + } + + return AnimationResult( + targetOffset - previousValue, + animationState, + ) +} + +private interface ApproachAnimation { + suspend fun approachAnimation( + scope: ScrollScope, + offset: T, + velocity: T, + onAnimationStep: (delta: T) -> Unit, + ): AnimationResult +} + +private fun Float.coerceToTarget(target: Float): Float { + if (target == 0f) return 0f + return if (target > 0) coerceAtMost(target) else coerceAtLeast(target) +} + +private class AnimationResult( + val remainingOffset: T, + val currentAnimationState: AnimationState, +) { + operator fun component1(): T = remainingOffset + operator fun component2(): AnimationState = currentAnimationState +} + +@Suppress("TopLevelPropertyNaming") +private const val DefaultScrollMotionDurationScaleFactor = 1f + +@Suppress("TopLevelPropertyNaming") +val DefaultScrollMotionDurationScale = object : MotionDurationScale { + override val scaleFactor: Float + get() = DefaultScrollMotionDurationScaleFactor +} + +@Suppress("TopLevelPropertyNaming") +internal val MinFlingVelocityDp = 400.dp + +@Suppress("TopLevelPropertyNaming") +internal const val NoDistance = 0f + +@Suppress("TopLevelPropertyNaming") +internal const val NoVelocity = 0f + +@ExperimentalFoundationApi +interface SnapLayoutInfoProvider { + /** + * The minimum offset that snapping will use to animate.(e.g. an item size) + */ + fun Density.calculateSnapStepSize(): Float + + /** + * Calculate the distance to navigate before settling into the next snapping bound. + * + * @param initialVelocity The current fling movement velocity. You can use this tho calculate a + * velocity based offset. + */ + fun Density.calculateApproachOffset(initialVelocity: Float): Float + + /** + * Given a target placement in a layout, the snapping offset is the next snapping position + * this layout can be placed in. If this is a short snapping, [currentVelocity] is guaranteed + * to be 0.If it is a long snapping, this method will be called + * after [calculateApproachOffset]. + * + * @param currentVelocity The current fling movement velocity. This may change throughout the + * fling animation. + */ + fun Density.calculateSnappingOffset(currentVelocity: Float): Float +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TangemSnapLayoutInfoProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TangemSnapLayoutInfoProvider.kt new file mode 100644 index 0000000000..1d68cf0349 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TangemSnapLayoutInfoProvider.kt @@ -0,0 +1,177 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.animation.core.calculateTargetValue +import androidx.compose.animation.splineBasedDecay +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior +import androidx.compose.foundation.lazy.LazyListLayoutInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.ui.unit.Density +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract +import kotlin.math.abs +import kotlin.math.absoluteValue +import kotlin.math.sign + +/** + * A [SnapLayoutInfoProvider] for LazyLists. + * + * @param lazyListState The [LazyListState] with information about the current state of the list + * @param positionInLayout The desired positioning of the snapped item within the main layout. + * This position should be considered with regard to the start edge of the item and the placement + * within the viewport. + * + * @return A [SnapLayoutInfoProvider] that can be used with [SnapFlingBehavior] + */ +@Suppress("FunctionNaming") +@ExperimentalFoundationApi +fun TangemSnapLayoutInfoProvider( + lazyListState: LazyListState, + positionInLayout: SnapPositionInLayout = SnapPositionInLayout.CenterToCenter, +): SnapLayoutInfoProvider = object : SnapLayoutInfoProvider { + + private val layoutInfo: LazyListLayoutInfo + get() = lazyListState.layoutInfo + + // Decayed page snapping is the default + override fun Density.calculateApproachOffset(initialVelocity: Float): Float { + val decayAnimationSpec: DecayAnimationSpec = splineBasedDecay(this) + val offset = + decayAnimationSpec.calculateTargetValue(NoDistance, initialVelocity).absoluteValue + val finalDecayOffset = (offset - calculateSnapStepSize()).coerceAtLeast(0f) + return if (finalDecayOffset == 0f) { + finalDecayOffset + } else { + finalDecayOffset * initialVelocity.sign + } + } + + override fun Density.calculateSnappingOffset(currentVelocity: Float): Float { + var lowerBoundOffset = Float.NEGATIVE_INFINITY + var upperBoundOffset = Float.POSITIVE_INFINITY + + layoutInfo.visibleItemsInfo.fastForEach { item -> + val offset = + calculateDistanceToDesiredSnapPosition( + mainAxisViewPortSize = layoutInfo.singleAxisViewportSize, + beforeContentPadding = layoutInfo.beforeContentPadding, + afterContentPadding = layoutInfo.afterContentPadding, + itemSize = item.size, + itemOffset = item.offset, + itemIndex = item.index, + snapPositionInLayout = positionInLayout, + ) + + // Find item that is closest to the center + if (offset <= 0 && offset > lowerBoundOffset) { + lowerBoundOffset = offset + } + + // Find item that is closest to center, but after it + if (offset >= 0 && offset < upperBoundOffset) { + upperBoundOffset = offset + } + } + + return calculateFinalOffset(currentVelocity, lowerBoundOffset, upperBoundOffset) + } + + override fun Density.calculateSnapStepSize(): Float = with(layoutInfo) { + if (visibleItemsInfo.isNotEmpty()) { + visibleItemsInfo.fastSumBy { it.size } / visibleItemsInfo.size.toFloat() + } else { + 0f + } + } +} + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +inline fun List.fastSumBy(selector: (T) -> Int): Int { + contract { callsInPlace(selector) } + var sum = 0 + fastForEach { element -> + sum += selector(element) + } + return sum +} + +internal fun calculateFinalOffset(velocity: Float, lowerBound: Float, upperBound: Float): Float { + fun Float.isValidDistance(): Boolean { + return this != Float.POSITIVE_INFINITY && this != Float.NEGATIVE_INFINITY + } + + val finalDistance = when (sign(velocity)) { + 0f -> { + if (abs(upperBound) <= abs(lowerBound)) { + upperBound + } else { + lowerBound + } + } + + 1f -> upperBound + -1f -> lowerBound + else -> NoDistance + } + + return if (finalDistance.isValidDistance()) { + finalDistance + } else { + NoDistance + } +} + +internal val LazyListLayoutInfo.singleAxisViewportSize: Int + get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width + +@Suppress("BanInlineOptIn") +@OptIn(ExperimentalContracts::class) +inline fun List.fastForEach(action: (T) -> Unit) { + contract { callsInPlace(action) } + for (index in indices) { + val item = get(index) + action(item) + } +} + +@Suppress("LongParameterList") +@OptIn(ExperimentalFoundationApi::class) +internal fun Density.calculateDistanceToDesiredSnapPosition( + mainAxisViewPortSize: Int, + beforeContentPadding: Int, + afterContentPadding: Int, + itemSize: Int, + itemOffset: Int, + itemIndex: Int, + snapPositionInLayout: SnapPositionInLayout, +): Float { + val containerSize = mainAxisViewPortSize - beforeContentPadding - afterContentPadding + + val desiredDistance = with(snapPositionInLayout) { + position(containerSize, itemSize, itemIndex) + }.toFloat() + + return itemOffset - desiredDistance +} + +@ExperimentalFoundationApi +fun interface SnapPositionInLayout { + /** + * Calculates an offset positioning between a container and an element within this container. + * The offset calculation is the necessary diff that should be applied to the item offset to + * align the item with a position within the container. As a base line, if we wanted to align + * the start of the container and the start of the item, we would return 0 in this function. + */ + fun Density.position(layoutSize: Int, itemSize: Int, itemIndex: Int): Int + + companion object { + /** + * Aligns the center of the item with the center of the containing layout. + */ + val CenterToCenter = + SnapPositionInLayout { layoutSize, itemSize, _ -> layoutSize / 2 - itemSize / 2 } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index bd417d7a27..c02a45e56e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -4,8 +4,6 @@ import androidx.compose.animation.core.* import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior -import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -22,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData @@ -30,6 +29,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.common.Wallet import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList +private const val SHORT_SNAP_ELEMENT_COUNT = 25 + /** * Wallets list component * @@ -53,7 +54,7 @@ internal fun WalletsList( state = lazyListState, contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState), + flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState, itemWidth = itemWidth), ) { items( items = wallets, @@ -80,17 +81,19 @@ internal fun WalletsList( */ @OptIn(ExperimentalFoundationApi::class) @Composable -private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState): SnapFlingBehavior { - val snappingLayout = remember(lazyListState) { SnapLayoutInfoProvider(lazyListState) } +private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState, itemWidth: Dp): TangemSnapFlingBehavior { + val snappingLayout = remember(lazyListState) { TangemSnapLayoutInfoProvider(lazyListState) } val density = LocalDensity.current val highVelocityApproachSpec: DecayAnimationSpec = rememberSplineBasedDecay() return remember(key1 = snappingLayout, key2 = highVelocityApproachSpec, key3 = density) { - SnapFlingBehavior( + TangemSnapFlingBehavior( snapLayoutInfoProvider = snappingLayout, lowVelocityAnimationSpec = tween(durationMillis = 1000, easing = LinearEasing), highVelocityAnimationSpec = highVelocityApproachSpec, snapAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow), + density = density, + shortSnapVelocityThreshold = itemWidth * SHORT_SNAP_ELEMENT_COUNT, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt index 939886db4a..63fbab40fd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.ui.utils -import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListState import kotlinx.coroutines.flow.FlowCollector import kotlin.math.abs @@ -20,14 +19,14 @@ internal class ScrollOffsetCollector( selectedWalletIndex: Int, private val lazyListState: LazyListState, private val onWalletChange: (Int) -> Unit, -) : FlowCollector> { +) : FlowCollector> { - private val LazyListItemInfo.halfItemSize + private val LazyListItemData.halfItemSize get() = size.div(other = 2) private var currentIndex = selectedWalletIndex - override suspend fun emit(value: List) { + override suspend fun emit(value: List) { if (!lazyListState.isScrollInProgress || value.size <= 1) return val firstItem = value.firstOrNull() ?: return @@ -46,4 +45,10 @@ internal class ScrollOffsetCollector( onWalletChange(newIndex) } } -} \ No newline at end of file +} + +internal data class LazyListItemData( + val index: Int, + val size: Int, + val offset: Int, +) \ No newline at end of file From 8ceec042540fe8bf218337957bb32b67256d0e55 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Feb 2024 10:07:17 +0100 Subject: [PATCH 11/33] Updated on 2026-08-14 --- .../presentation/wallet/ui/WalletScreen.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 8341a0792c..c7913c1fc6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -64,6 +64,13 @@ internal fun WalletScreen( val snackbarHostState = remember(::SnackbarHostState) val isAutoScroll = remember { mutableStateOf(value = false) } + var alertConfig by remember { mutableStateOf(value = null) } + + val config = alertConfig + if (config != null) { + WalletAlert(state = config, onDismiss = { alertConfig = null }) + } + WalletContent( state = state, walletsListState = walletsListState, @@ -72,14 +79,9 @@ internal fun WalletScreen( onAutoScrollReset = { isAutoScroll.value = false }, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, bottomSheetContent = bottomSheetContent, + alertConfig = alertConfig, ) - var alertConfig by remember { mutableStateOf(value = null) } - - alertConfig?.let { - WalletAlert(state = it, onDismiss = { alertConfig = null }) - } - WalletEventEffect( event = state.event, selectedWalletIndex = state.selectedWalletIndex, @@ -100,6 +102,7 @@ private fun WalletContent( bottomSheetHeaderHeightProvider: () -> Dp, onAutoScrollReset: () -> Unit, bottomSheetContent: @Composable () -> Unit, + alertConfig: WalletAlertState?, ) { var selectedWalletIndex by remember { mutableIntStateOf(state.selectedWalletIndex) } val selectedWallet = state.wallets[selectedWalletIndex] @@ -207,6 +210,7 @@ private fun WalletContent( snackbarHostState = snackbarHostState, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, bottomSheetContent = bottomSheetContent, + alertConfig = alertConfig, ) { scaffoldContent() } @@ -230,6 +234,7 @@ private fun BaseScaffoldManageTokenRedesign( snackbarHostState: SnackbarHostState, bottomSheetHeaderHeightProvider: () -> Dp, bottomSheetContent: @Composable () -> Unit, + alertConfig: WalletAlertState?, content: @Composable () -> Unit, ) { val scaffoldState = rememberBottomSheetScaffoldState() @@ -252,7 +257,7 @@ private fun BaseScaffoldManageTokenRedesign( val keyboardShown by keyboardAsState() // expand bottom sheet when keyboard appears LaunchedEffect(keyboardShown is Keyboard.Opened) { - if (keyboardShown is Keyboard.Opened) { + if (keyboardShown is Keyboard.Opened && alertConfig == null) { scaffoldState.bottomSheetState.expand() } } From 6948168b9c5138e603932305a30f859b0cd856e3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Feb 2024 17:42:35 +0300 Subject: [PATCH 12/33] Updated on 2026-08-14 --- .../ui/AddCustomTokenBottomSheet.kt | 90 ++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt index 499c2e6cb4..69fe567327 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt @@ -1,13 +1,22 @@ package com.tangem.managetokens.presentation.addcustomtoken.ui import android.annotation.SuppressLint +import androidx.activity.compose.BackHandler +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.systemBars +import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.ModalBottomSheetDefaults +import androidx.compose.material3.ModalBottomSheetProperties +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -15,8 +24,18 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -29,6 +48,7 @@ import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomToken import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomTokenRouter import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenViewModel import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -46,14 +66,14 @@ fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig) { onDispose { viewModel.onDispose() } } - // FIXME: handle back presses after updating material3 to 1.2.0 - ModalBottomSheet( + ModalBottomSheetWithBackHandling( onDismissRequest = config.onDismissRequest, sheetState = sheetState, containerColor = TangemTheme.colors.background.tertiary, shape = TangemTheme.shapes.bottomSheetLarge, windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top), dragHandle = { TangemBottomSheetDraggableHeader(color = TangemTheme.colors.background.tertiary) }, + properties = ModalBottomSheetDefaults.properties(shouldDismissOnBackPress = false), ) { Content(onDismissRequest = config.onDismissRequest, viewModel = viewModel) } @@ -68,6 +88,68 @@ fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig) { } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ModalBottomSheetWithBackHandling( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + containerColor: Color = BottomSheetDefaults.ContainerColor, + shape: Shape = BottomSheetDefaults.ExpandedShape, + windowInsets: WindowInsets = BottomSheetDefaults.windowInsets, + dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, + sheetState: SheetState = rememberModalBottomSheetState(), + properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties(), + content: @Composable ColumnScope.() -> Unit, +) { + val scope = rememberCoroutineScope() + + BackHandler(enabled = sheetState.targetValue != SheetValue.Hidden) { + // Always catch back here, but only let it dismiss if shouldDismissOnBackPress. + // If not, it will have no effect. + if (properties.shouldDismissOnBackPress) { + scope.launch { sheetState.hide() }.invokeOnCompletion { + if (!sheetState.isVisible) { + onDismissRequest() + } + } + } + } + + val requester = remember { FocusRequester() } + val backPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + containerColor = containerColor, + shape = shape, + windowInsets = windowInsets, + dragHandle = dragHandle, + sheetState = sheetState, + modifier = modifier + .focusRequester(requester) + .focusable() + .onPreviewKeyEvent { + if (it.key == Key.Back && it.type == KeyEventType.KeyUp && !it.nativeKeyEvent.isCanceled) { + backPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() + return@onPreviewKeyEvent true + } + return@onPreviewKeyEvent false + }, + properties = ModalBottomSheetDefaults.properties( + securePolicy = properties.securePolicy, + isFocusable = properties.isFocusable, + // Set false otherwise the onPreviewKeyEvent doesn't work at all. + // The functionality of shouldDismissOnBackPress is achieved by the BackHandler. + shouldDismissOnBackPress = false, + ), + content = content, + ) + + LaunchedEffect(Unit) { + requester.requestFocus() + } +} + @SuppressLint("RestrictedApi") @Composable private fun Content(viewModel: AddCustomTokenViewModel, onDismissRequest: () -> Unit) { @@ -82,6 +164,10 @@ private fun Content(viewModel: AddCustomTokenViewModel, onDismissRequest: () -> } } + BackHandler(true) { + navController.popBackStack() + } + val router = remember(navController) { AddCustomTokenRouter(navController) } viewModel.router = router From eb290ea97cbc02389c9c0080228a2c79ec9f2147 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Feb 2024 14:58:32 +0000 Subject: [PATCH 13/33] Updated on 2026-08-14 --- .../tokens/UpdateDelayedNetworkStatusUseCase.kt | 2 +- .../viewmodels/TokenDetailsViewModel.kt | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt index 1991a33bf8..26d641497a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt @@ -33,7 +33,7 @@ class UpdateDelayedNetworkStatusUseCase( suspend operator fun invoke( userWalletId: UserWalletId, network: Network, - delayMillis: Long, + delayMillis: Long = 0L, refresh: Boolean = false, ): Either { delay(delayMillis) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 382d9ca76e..fbdbcf8273 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -43,6 +43,7 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsCurrencyStatusAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender @@ -84,6 +85,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, + private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, private val quotesRepository: QuotesRepository, @@ -301,6 +303,9 @@ internal class TokenDetailsViewModel @Inject constructor( val config = uiState.bottomSheetConfig val exchangeBottomSheet = config?.content as? ExchangeStatusBottomSheetConfig val currentTx = swapTxs.firstOrNull { it.txId == exchangeBottomSheet?.value?.txId } + if (currentTx?.activeStatus == ExchangeStatus.Finished) { + updateNetworkToSwapBalance(currentTx.toCryptoCurrency) + } uiState = uiState.copy( swapTxs = swapTxs, bottomSheetConfig = currentTx?.let( @@ -309,6 +314,16 @@ internal class TokenDetailsViewModel @Inject constructor( ) } + private fun updateNetworkToSwapBalance(toCryptoCurrency: CryptoCurrency) { + viewModelScope.launch { + updateDelayedCurrencyStatusUseCase( + userWalletId = userWalletId, + network = toCryptoCurrency.network, + refresh = true, + ) + } + } + /** * @param refresh - invalidate cache and get data from remote * @param showItemsLoading - show loading items placeholder. From 15fb5b3954290438ba1a071b3a993df077498779 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 20 Feb 2024 14:04:49 +0000 Subject: [PATCH 14/33] Updated on 2026-08-14 --- .../core/ui/extensions/BlockchainIcons.kt | 10 ++--- .../src/main/res/drawable/ic_dischain_22.xml | 26 ++++++++++++ .../main/res/drawable/ic_ethereumfair_22.xml | 37 ----------------- .../main/res/drawable/ic_ethereumpow_22.xml | 10 ++--- .../src/main/res/drawable/img_dischain_22.xml | 28 +++++++++++++ .../src/main/res/drawable/img_eth_fair_22.xml | 40 ------------------- .../utils/ResponseCryptoCurrenciesFactory.kt | 10 ++++- .../domain/common/extensions/Blockchain.kt | 6 +-- gradle/dependencies.toml | 2 +- 9 files changed, 76 insertions(+), 93 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_dischain_22.xml delete mode 100644 core/ui/src/main/res/drawable/ic_ethereumfair_22.xml create mode 100644 core/ui/src/main/res/drawable/img_dischain_22.xml delete mode 100644 core/ui/src/main/res/drawable/img_eth_fair_22.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index c5a3195582..ea2e3141bf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -27,7 +27,7 @@ fun getActiveIconRes(blockchainId: String): Int { "TRON", "TRON/test" -> R.drawable.img_tron_22 "GNO" -> R.drawable.img_gnosis_22 "ETH-Pow", "ETH-Pow/test" -> R.drawable.img_eth_pow_22 - "ETH-Fair" -> R.drawable.img_eth_fair_22 + "ETH-Fair", "dischain" -> R.drawable.img_dischain_22 "Polkadot", "Polkadot/test" -> R.drawable.img_polkadot_22 "Kusama" -> R.drawable.img_kusama_22 "OPTIMISM", "OPTIMISM/test" -> R.drawable.img_optimism_22 @@ -81,7 +81,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "tron", "tron/test" -> R.drawable.img_tron_22 "xdai" -> R.drawable.img_gnosis_22 "ethereum-pow-iou", "ethereum-pow-iou/test" -> R.drawable.img_eth_pow_22 - "ethereumfair" -> R.drawable.img_eth_fair_22 + "ethereumfair", "dischain" -> R.drawable.img_dischain_22 "polkadot", "polkadot/test" -> R.drawable.img_polkadot_22 "kusama" -> R.drawable.img_kusama_22 "optimistic-ethereum", "optimistic-ethereum/test" -> R.drawable.img_optimism_22 @@ -137,7 +137,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "dogecoin" -> R.drawable.img_dogecoin_22 "xdai" -> R.drawable.img_gnosis_22 "ethereum-pow-iou" -> R.drawable.img_eth_pow_22 - "ethereumfair" -> R.drawable.img_eth_fair_22 + "ethereumfair", "dischain" -> R.drawable.img_dischain_22 "kusama" -> R.drawable.img_kusama_22 "dash" -> R.drawable.img_dash_22 "kaspa" -> R.drawable.img_kaspa_22 @@ -186,7 +186,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "TRON", "TRON/test" -> R.drawable.ic_tron_22 "GNO" -> R.drawable.ic_gnosis_22 "ETH-Pow", "ETH-Pow/test" -> R.drawable.ic_ethereumpow_22 - "ETH-Fair" -> R.drawable.ic_ethereumfair_22 + "ETH-Fair", "dischain" -> R.drawable.ic_dischain_22 "Polkadot", "Polkadot/test" -> R.drawable.ic_polkadot_16 "Kusama" -> R.drawable.ic_kusama_16 "OPTIMISM", "OPTIMISM/test" -> R.drawable.ic_optimism_22 @@ -240,7 +240,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "tron", "tron/test" -> R.drawable.ic_tron_22 "xdai" -> R.drawable.ic_gnosis_22 "ethereum-pow-iou", "ethereum-pow-iou/test" -> R.drawable.ic_ethereumpow_22 - "ethereumfair" -> R.drawable.ic_ethereumfair_22 + "ethereumfair", "dischain" -> R.drawable.ic_dischain_22 "polkadot", "polkadot/test" -> R.drawable.ic_polkadot_16 "kusama" -> R.drawable.ic_kusama_16 "optimistic-ethereum", "optimistic-ethereum/test" -> R.drawable.ic_optimism_22 diff --git a/core/ui/src/main/res/drawable/ic_dischain_22.xml b/core/ui/src/main/res/drawable/ic_dischain_22.xml new file mode 100644 index 0000000000..5387a21c0c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_dischain_22.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_ethereumfair_22.xml b/core/ui/src/main/res/drawable/ic_ethereumfair_22.xml deleted file mode 100644 index d98f048426..0000000000 --- a/core/ui/src/main/res/drawable/ic_ethereumfair_22.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - diff --git a/core/ui/src/main/res/drawable/ic_ethereumpow_22.xml b/core/ui/src/main/res/drawable/ic_ethereumpow_22.xml index 40a0651a32..f4af8483ac 100644 --- a/core/ui/src/main/res/drawable/ic_ethereumpow_22.xml +++ b/core/ui/src/main/res/drawable/ic_ethereumpow_22.xml @@ -9,19 +9,19 @@ diff --git a/core/ui/src/main/res/drawable/img_dischain_22.xml b/core/ui/src/main/res/drawable/img_dischain_22.xml new file mode 100644 index 0000000000..876950bc88 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_dischain_22.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_eth_fair_22.xml b/core/ui/src/main/res/drawable/img_eth_fair_22.xml deleted file mode 100644 index b8e6604a7a..0000000000 --- a/core/ui/src/main/res/drawable/img_eth_fair_22.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt index fde4c4be2e..5157d1430d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt @@ -74,11 +74,17 @@ class ResponseCryptoCurrenciesFactory { ): CryptoCurrency.Coin? { val network = getNetwork(blockchain, responseToken.derivationPath, derivationStyleProvider) ?: return null + // workaround: Dischain was renamed but backend still returns the old name, + // get name and symbol from enum Blockchain until backend renamed + // [REDACTED_JIRA] + val name = if (blockchain == Blockchain.Dischain) blockchain.fullName else responseToken.name + val symbol = if (blockchain == Blockchain.Dischain) blockchain.currency else responseToken.symbol + return CryptoCurrency.Coin( id = getCoinId(network, blockchain.toCoinId()), network = network, - name = responseToken.name, - symbol = responseToken.symbol, + name = name, + symbol = symbol, decimals = responseToken.decimals, iconUrl = getCoinIconUrl(blockchain), isCustom = isCustomCoin(network), diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 86fc4fd0be..8ecb812304 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -43,7 +43,7 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "xdai" -> Blockchain.Gnosis "ethereum-pow-iou" -> Blockchain.EthereumPow "ethereum-pow-iou/test" -> Blockchain.EthereumPowTestnet - "ethereumfair" -> Blockchain.EthereumFair + "ethereumfair", "dischain" -> Blockchain.Dischain // for old client compatibility "polkadot" -> Blockchain.Polkadot "polkadot/test" -> Blockchain.PolkadotTestnet "kusama" -> Blockchain.Kusama @@ -131,7 +131,7 @@ fun Blockchain.toNetworkId(): String { Blockchain.Gnosis -> "xdai" Blockchain.EthereumPow -> "ethereum-pow-iou" Blockchain.EthereumPowTestnet -> "ethereum-pow-iou/test" - Blockchain.EthereumFair -> "ethereumfair" + Blockchain.Dischain -> "ethereumfair" // for backend compatibility Blockchain.Polkadot -> "polkadot" Blockchain.PolkadotTestnet -> "polkadot/test" Blockchain.Kusama -> "kusama" @@ -203,7 +203,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Dogecoin -> "dogecoin" Blockchain.Gnosis -> "xdai" Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> "ethereum-pow-iou" - Blockchain.EthereumFair -> "ethereumfair" + Blockchain.Dischain -> "ethereumfair" // for backend compatibility Blockchain.Kusama -> "kusama" Blockchain.Optimism, Blockchain.OptimismTestnet -> "optimistic-ethereum" Blockchain.Dash -> "dash" diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index f4b5153615..47d3487f7f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ web3j = "4.10.1" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-495" +tangemBlockchainSdk = "develop-497" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-324" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From ff1ac5d0e7b708f494385e44b686ab8541ebbb80 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Feb 2024 13:42:29 +0100 Subject: [PATCH 15/33] Updated on 2026-08-14 --- .../viewmodels/AddCustomTokenViewModel.kt | 2 +- .../viewmodels/TokensListMigration.kt | 6 +- .../tokens/GetCryptoCurrenciesUseCase.kt | 13 ++- .../viewmodels/AddCustomTokenViewModel.kt | 2 +- .../viewmodels/ManageTokensViewModel.kt | 91 ++++++++++++------- .../presentation/viewmodel/SendViewModel.kt | 2 +- 6 files changed, 79 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 0825668edb..b15a5fdee5 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -87,7 +87,7 @@ internal class AddCustomTokenViewModel @Inject constructor( currentCryptoCurrencies = getSelectedWalletSyncUseCase().fold( ifLeft = { emptyList() }, ifRight = { selectedWallet -> - getCurrenciesUseCase(selectedWallet.walletId).fold( + getCurrenciesUseCase.getSync(selectedWallet.walletId).fold( ifLeft = { emptyList() }, ifRight = { it }, ) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt index f4f5e5e72d..0768165d5b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -49,7 +49,11 @@ internal class TokensListMigration( is Either.Right -> { currentUserWallet = selectedWalletEither.value - when (val currenciesEither = getCurrenciesUseCase(userWalletId = selectedWalletEither.value.walletId)) { + when ( + val currenciesEither = getCurrenciesUseCase.getSync( + userWalletId = selectedWalletEither.value.walletId, + ) + ) { is Either.Left -> { Timber.e(currenciesEither.value.toString()) TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList()) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt index 1ead96bd8a..f0c73552f7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt @@ -1,16 +1,27 @@ package com.tangem.domain.tokens import arrow.core.Either +import arrow.core.left import arrow.core.raise.catch import arrow.core.raise.either +import arrow.core.right import com.tangem.domain.tokens.error.GetCurrenciesError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map class GetCryptoCurrenciesUseCase(private val currenciesRepository: CurrenciesRepository) { - suspend operator fun invoke( + operator fun invoke(userWalletId: UserWalletId): Flow>> { + return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWalletId) + .map, Either>> { it.right() } + .catch { emit(GetCurrenciesError.DataError(it).left()) } + } + + suspend fun getSync( userWalletId: UserWalletId, refresh: Boolean = false, ): Either> { diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt index 1efdf2911b..71cb37a5e9 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt @@ -424,7 +424,7 @@ internal class AddCustomTokenViewModel @Inject constructor( selectedWallet: UserWallet, cryptoCurrency: CryptoCurrency, ): Boolean { - val currenciesList = getCurrenciesUseCase(selectedWallet.walletId).getOrElse { emptyList() } + val currenciesList = getCurrenciesUseCase.getSync(selectedWallet.walletId).getOrElse { emptyList() } return when (cryptoCurrency) { is CryptoCurrency.Coin -> { currenciesList.any { diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt index ad42c3159e..09ced5cf54 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt @@ -33,11 +33,15 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.Debouncer.Companion.DEFAULT_WAIT_TIME_MS +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.plus -import kotlinx.coroutines.withContext +import timber.log.Timber +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject import kotlin.collections.set import kotlin.properties.Delegates @@ -62,8 +66,6 @@ internal class ManageTokensViewModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, ) : ViewModel(), ManageTokensClickIntents, ManageTokensUiEvents { - private val debouncer = Debouncer() - private val stateFactory = ManageTokensStateFactory( currentStateProvider = Provider { uiState }, clickIntents = this, @@ -73,11 +75,17 @@ internal class ManageTokensViewModel @Inject constructor( var uiState: ManageTokensState by mutableStateOf(stateFactory.getInitialState(flowOf(PagingData.from(emptyList())))) private set - private var allAddedCurrencies: MutableList = mutableListOf() + private val currenciesListJobHolder: JobHolder = JobHolder() - private var wallets: List by Delegates.notNull() + private val debouncer = Debouncer() - private var addedCurrenciesByWallet: MutableMap> = mutableMapOf() + private var allAddedCurrencies: MutableList = Collections.synchronizedList( + mutableListOf(), + ) + + private var wallets: CopyOnWriteArrayList by Delegates.notNull() + + private var addedCurrenciesByWallet: MutableMap> = ConcurrentHashMap() private var selectedWallet: UserWallet? = null @@ -114,34 +122,53 @@ internal class ManageTokensViewModel @Inject constructor( getWalletsUseCase() .distinctUntilChanged() .collectLatest { userWallets -> - wallets = userWallets.filter { it.isMultiCurrency && !it.isLocked } - wallets.map { wallet -> - val currencies = getCurrenciesUseCase(wallet.walletId).fold( - ifLeft = { emptyList() }, - ifRight = { it }, - ) - allAddedCurrencies += currencies - addedCurrenciesByWallet[wallet] = currencies.toMutableList() - } - withContext(dispatchers.main) { - uiState = uiState.copy(tokens = getInitialTokensList()) - } - selectedWallet = getSelectedWalletSyncUseCase().fold( - ifLeft = { null }, - ifRight = { if (!it.isMultiCurrency || it.isLocked) null else it }, - ) - if (selectedWallet == null && wallets.isNotEmpty()) { - selectWalletUseCase(wallets.first().walletId) - selectedWallet = wallets.first() - } - updateDerivationNotificationState() - withContext(dispatchers.main) { - uiState = stateFactory.updateChooseWalletState(wallets, userWallets, selectedWallet) - } + launch { + subscribeToCurrencies(userWallets) + }.saveIn(currenciesListJobHolder) } } } + private suspend fun subscribeToCurrencies(userWallets: List) { + wallets = CopyOnWriteArrayList(userWallets.filter { it.isMultiCurrency && !it.isLocked }) + + combine(wallets.map { getCurrenciesUseCase.invoke(it.walletId).distinctUntilChanged() }) { + allAddedCurrencies.clear() + addedCurrenciesByWallet.clear() + + val walletsWithCurrencies = wallets.zip( + it.map { currencyList -> + currencyList.getOrElse { + Timber.e("Couldn't retrieve currency list") + emptyList() + } + }, + ) + + allAddedCurrencies = walletsWithCurrencies.flatMap { it.second }.toMutableList() + + walletsWithCurrencies.forEach { (wallet, currencies) -> + addedCurrenciesByWallet[wallet] = currencies.toMutableList() + } + + withContext(dispatchers.main) { + uiState = uiState.copy(tokens = getInitialTokensList()) + } + selectedWallet = getSelectedWalletSyncUseCase().fold( + ifLeft = { null }, + ifRight = { if (!it.isMultiCurrency || it.isLocked) null else it }, + ) + if (selectedWallet == null && wallets.isNotEmpty()) { + selectWalletUseCase(wallets.first().walletId) + selectedWallet = wallets.first() + } + updateDerivationNotificationState() + withContext(dispatchers.main) { + uiState = stateFactory.updateChooseWalletState(wallets, userWallets, selectedWallet) + } + }.collect() + } + private fun getInitialTokensList(searchText: String = ""): Flow> { return getGlobalTokenListUseCase(searchText = searchText).map { it.map { token -> tokenConverter.convert(token) } 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 8a0f1440e1..524d95dada 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 @@ -324,7 +324,7 @@ internal class SendViewModel @Inject constructor( .filterNot { it.walletId == userWalletId || it.isLocked } .map { wallet -> async(dispatchers.io) { - getCryptoCurrenciesUseCase(wallet.walletId) + getCryptoCurrenciesUseCase.getSync(wallet.walletId) .fold( ifRight = { currencyItem -> val walletCurrency = currencyItem.firstOrNull { From 283bc49d3a0518b318eabc7ae915972682535121 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 18 Feb 2024 14:37:31 +0300 Subject: [PATCH 16/33] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 5 + .../tap/common/redux/global/GlobalReducer.kt | 4 +- .../domain/tasks/product/ScanProductTask.kt | 36 +++-- .../features/onboarding/OnboardingHelper.kt | 12 +- .../features/onboarding/OnboardingManager.kt | 29 ++-- .../note/redux/OnboardingNoteMiddleware.kt | 15 +- .../redux/OnboardingOtherCardsMiddleware.kt | 15 +- .../twins/redux/TwinCardsMiddleware.kt | 72 +++++---- .../redux/OnboardingWalletMiddleware.kt | 89 +++++------ .../tap/proxy/redux/DaggerGraphState.kt | 2 + .../di/AppPreferencesStoreModule.kt | 2 +- .../local/preferences/AppPreferencesStore.kt | 11 +- .../tangem/data/card/DefaultCardRepository.kt | 78 +++++++-- .../preferences/PreferencesDataSource.kt | 11 -- .../model/DataSourceUsedCardInfo.kt | 8 - .../model/DataSourceUsedCardInfoOld.kt | 7 - .../storage/UsedCardsPrefStorage.kt | 150 ------------------ .../domain/card/repository/CardRepository.kt | 15 ++ 18 files changed, 247 insertions(+), 314 deletions(-) delete mode 100644 data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/model/DataSourceUsedCardInfo.kt delete mode 100644 data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/model/DataSourceUsedCardInfoOld.kt delete mode 100644 data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/UsedCardsPrefStorage.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 83452572ae..794eed1b33 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -30,6 +30,7 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.LogConfig import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase @@ -170,6 +171,9 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase + + @Inject + lateinit var cardRepository: CardRepository // endregion Injected override fun onCreate() { @@ -256,6 +260,7 @@ internal class TapApplication : Application(), ImageLoaderFactory { generalUserWalletsListManager = generalUserWalletsListManager, wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase, saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, + cardRepository = cardRepository, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 778a37590c..fbe3e38b89 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -7,7 +7,6 @@ import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.onboarding.OnboardingManager -import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store @@ -22,8 +21,7 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde return when (action) { is GlobalAction.Onboarding.Start -> { - val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage - val onboardingManager = OnboardingManager(action.scanResponse, usedCardsPrefStorage) + val onboardingManager = OnboardingManager(action.scanResponse) globalState.copy(onboardingState = OnboardingState(true, onboardingManager)) } is GlobalAction.Onboarding.StartForUnfinishedBackup -> { diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 8d811541cf..8f065eda68 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -31,9 +31,12 @@ import com.tangem.operations.backup.StartPrimaryCardLinkingTask import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.files.ReadFilesTask import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand +import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.TapSdkError -import com.tangem.tap.preferencesStorage +import com.tangem.tap.mainScope +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope +import com.tangem.tap.store import kotlinx.coroutines.launch import kotlin.collections.set @@ -185,23 +188,28 @@ private class ScanWalletProcessor( session: CardSession, callback: (result: CompletionResult) -> Unit, ) { - val activationInProgress = preferencesStorage.usedCardsPrefStorage.isActivationInProgress(card.cardId) + mainScope.launch { + val activationInProgress = store.inject(DaggerGraphState::cardRepository) + .isActivationInProgress(card.cardId) - @Suppress("ComplexCondition") - if (card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty() && activationInProgress) { - StartPrimaryCardLinkingTask().run(session) { linkingResult -> - when (linkingResult) { - is CompletionResult.Success -> { - primaryCard = linkingResult.data - deriveKeysIfNeeded(card, session, callback) - } - is CompletionResult.Failure -> { - deriveKeysIfNeeded(card, session, callback) + @Suppress("ComplexCondition") + if (card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty() && + activationInProgress + ) { + StartPrimaryCardLinkingTask().run(session) { linkingResult -> + when (linkingResult) { + is CompletionResult.Success -> { + primaryCard = linkingResult.data + deriveKeysIfNeeded(card, session, callback) + } + is CompletionResult.Failure -> { + deriveKeysIfNeeded(card, session, callback) + } } } + } else { + deriveKeysIfNeeded(card, session, callback) } - } else { - deriveKeysIfNeeded(card, session, callback) } } 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 fa2b2a0832..0d1788de88 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 @@ -29,26 +29,26 @@ import timber.log.Timber [REDACTED_AUTHOR] */ object OnboardingHelper { - fun isOnboardingCase(response: ScanResponse): Boolean { - val cardInfoStorage = preferencesStorage.usedCardsPrefStorage + suspend fun isOnboardingCase(response: ScanResponse): Boolean { + val onboardingManager = store.state.globalState.onboardingState.onboardingManager val cardId = response.card.cardId return when { response.cardTypesResolver.isTangemTwins() -> { if (!response.twinsIsTwinned()) { true } else { - cardInfoStorage.isActivationInProgress(cardId) + onboardingManager?.isActivationInProgress(cardId) ?: false } } response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> { val emptyWallets = response.card.wallets.isEmpty() - val activationInProgress = cardInfoStorage.isActivationInProgress(cardId) + val activationInProgress = onboardingManager?.isActivationInProgress(cardId) val backupNotActive = response.card.backupStatus?.isActive != true - emptyWallets || activationInProgress || backupNotActive + emptyWallets || activationInProgress == true || backupNotActive } - response.card.wallets.isNotEmpty() -> cardInfoStorage.isActivationInProgress(cardId) + response.card.wallets.isNotEmpty() -> onboardingManager?.isActivationInProgress(cardId) ?: false else -> true } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index 467d4d73a5..d37b7153d1 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -5,11 +5,12 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.isZero import com.tangem.common.services.Result -import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage +import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.models.scan.ScanResponse import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.entities.ProgressState +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.isPositive import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.domain.TapError @@ -17,17 +18,17 @@ import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl import com.tangem.tap.domain.model.Currency import com.tangem.tap.domain.model.hasPendingTransactions import com.tangem.tap.features.demo.isDemoCard +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.store import timber.log.Timber import java.math.BigDecimal /** [REDACTED_AUTHOR] */ -class OnboardingManager( - var scanResponse: ScanResponse, - private val usedCardsPrefStorage: UsedCardsPrefStorage, -) { +class OnboardingManager(var scanResponse: ScanResponse) { + private val cardRepository: CardRepository = store.inject(DaggerGraphState::cardRepository) private var cardInfo: Result? = null suspend fun loadArtworkUrl(): String { @@ -76,17 +77,23 @@ class OnboardingManager( ) } - fun activationStarted(cardId: String) { - usedCardsPrefStorage.activationStarted(cardId) + suspend fun startActivation(cardId: String) { + cardRepository.startCardActivation(cardId) } - fun activationFinished(cardId: String) { - usedCardsPrefStorage.activationFinished(cardId) + suspend fun finishActivation(cardId: String) { + cardRepository.finishCardActivation(cardId) } - fun isActivationStarted(cardId: String): Boolean { - return usedCardsPrefStorage.isActivationStarted(cardId) + suspend fun finishActivation(cardIds: List) { + cardRepository.finishCardsActivation(cardIds) } + + suspend fun isActivationStarted(cardId: String): Boolean = cardRepository.isActivationStarted(cardId) + + suspend fun isActivationFinished(cardId: String): Boolean = cardRepository.isActivationFinished(cardId) + + suspend fun isActivationInProgress(cardId: String): Boolean = cardRepository.isActivationInProgress(cardId) } data class OnboardingWalletBalance( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index cf01d56658..114dedc7df 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -20,6 +20,7 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper +import com.tangem.tap.mainScope import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager @@ -57,8 +58,10 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch when (action) { is OnboardingNoteAction.Init -> { - if (!onboardingManager.isActivationStarted(card.cardId)) { - Analytics.send(Onboarding.Started()) + scope.launch { + if (!onboardingManager.isActivationStarted(card.cardId)) { + Analytics.send(Onboarding.Started()) + } } } is OnboardingNoteAction.LoadCardArtwork -> { @@ -86,8 +89,10 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch } OnboardingNoteStep.Done -> { Analytics.send(Onboarding.Finished()) - onboardingManager.activationFinished(card.cardId) - postUi(DELAY_SDK_DIALOG_CLOSE) { store.dispatch(OnboardingNoteAction.Confetti.Show) } + mainScope.launch { + onboardingManager.finishActivation(card.cardId) + postUi(DELAY_SDK_DIALOG_CLOSE) { store.dispatch(OnboardingNoteAction.Confetti.Show) } + } } else -> Unit } @@ -101,7 +106,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) val updatedResponse = scanResponse.copy(card = result.data.card) onboardingManager.scanResponse = updatedResponse - onboardingManager.activationStarted(updatedResponse.card.cardId) + onboardingManager.startActivation(updatedResponse.card.cardId) store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.TopUpWallet)) } is CompletionResult.Failure -> Unit diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index 243319a1c0..d5e8144485 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -11,6 +11,7 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper +import com.tangem.tap.mainScope import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager @@ -43,8 +44,10 @@ private fun handleOtherCardsAction(action: Action) { when (action) { is OnboardingOtherCardsAction.Init -> { - if (!onboardingManager.isActivationStarted(card.cardId)) { - Analytics.send(Onboarding.Started()) + scope.launch { + if (!onboardingManager.isActivationStarted(card.cardId)) { + Analytics.send(Onboarding.Started()) + } } } is OnboardingOtherCardsAction.LoadCardArtwork -> { @@ -67,8 +70,10 @@ private fun handleOtherCardsAction(action: Action) { } OnboardingOtherCardsStep.Done -> { Analytics.send(Onboarding.Finished()) - onboardingManager.activationFinished(card.cardId) - postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) } + mainScope.launch { + onboardingManager.finishActivation(card.cardId) + postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) } + } } else -> Unit } @@ -88,7 +93,7 @@ private fun handleOtherCardsAction(action: Action) { ) val updatedCard = updatedResponse.card onboardingManager.scanResponse = updatedResponse - onboardingManager.activationStarted(updatedCard.cardId) + onboardingManager.startActivation(updatedCard.cardId) delay(DELAY_SDK_DIALOG_CLOSE) store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done)) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index 4a7906dfdb..90f14188f3 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -75,51 +75,61 @@ private fun handle(action: Action, dispatch: DispatchFunction) { fun startCardActivation(cardId: String) { if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) { - onboardingManager?.activationStarted(cardId) + scope.launch { + onboardingManager?.startActivation(cardId) + } } } fun finishCardActivation() { if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) { Analytics.send(Onboarding.Finished()) - onboardingManager?.activationFinished(getScanResponse().card.cardId) - twinCardsState.pairCardId?.let { onboardingManager?.activationFinished(it) } + + val cardIds = listOfNotNull(getScanResponse().card.cardId, twinCardsState.pairCardId) + + if (cardIds.isNotEmpty()) { + mainScope.launch { + onboardingManager?.finishActivation(cardIds) + } + } } } when (action) { is TwinCardsAction.Init -> { - if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return + mainScope.launch { + if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return@launch - val scanResponse = getScanResponse() - onboardingManager?.apply { - if (!isActivationStarted(scanResponse.card.cardId)) { - Analytics.send(Onboarding.Started()) - } - } - - when (twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> { - mainScope.launch { - val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) - .invokeSync() - - val dispatchAction = if (wasTwinsOnboardingShown) { - val step = when { - !scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet - twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done - else -> TwinCardsStep.TopUpWallet - } - TwinCardsAction.SetStepOfScreen(step) - } else { - TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome) - } - - store.dispatch(dispatchAction) + val scanResponse = getScanResponse() + onboardingManager?.apply { + if (!isActivationStarted(scanResponse.card.cardId)) { + Analytics.send(Onboarding.Started()) } } - CreateTwinWalletMode.RecreateWallet -> { - store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning)) + + when (twinCardsState.mode) { + CreateTwinWalletMode.CreateWallet -> { + mainScope.launch { + val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) + .invokeSync() + + val dispatchAction = if (wasTwinsOnboardingShown) { + val step = when { + !scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet + twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done + else -> TwinCardsStep.TopUpWallet + } + TwinCardsAction.SetStepOfScreen(step) + } else { + TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome) + } + + store.dispatch(dispatchAction) + } + } + CreateTwinWalletMode.RecreateWallet -> { + store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning)) + } } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 2a4ca101c4..ca0b30020d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -27,6 +27,7 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse @@ -72,8 +73,10 @@ private fun handleWalletAction(action: Action) { when (action) { OnboardingWalletAction.Init -> { ifNotNull(onboardingManager, card) { manager, notNullCard -> - if (!manager.isActivationStarted(notNullCard.cardId)) { - Analytics.send(Onboarding.Started()) + mainScope.launch { + if (!manager.isActivationStarted(notNullCard.cardId)) { + Analytics.send(Onboarding.Started()) + } } } when { @@ -144,8 +147,10 @@ private fun handleWalletAction(action: Action) { onboardingManager.scanResponse = updatedResponse store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false)) - startCardActivation(updatedResponse) - store.dispatch(OnboardingWalletAction.ResumeBackup) + mainScope.launch { + onboardingManager.startActivation(updatedResponse.card.cardId) + store.dispatchWithMain(OnboardingWalletAction.ResumeBackup) + } } is CompletionResult.Failure -> { if (result.error is TangemSdkError.WalletAlreadyCreated) { @@ -493,11 +498,13 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) backupService.discardSavedBackup() } is BackupAction.DiscardSavedBackup -> { - backupService.primaryCardId?.let { - Analytics.send(Onboarding.Finished()) - finishCardsActivationForDiscardedUnfinishedBackup(it) + mainScope.launch { + backupService.primaryCardId?.let { + Analytics.send(Onboarding.Finished()) + store.state.globalState.onboardingState.onboardingManager?.finishActivation(it) + } + backupService.discardSavedBackup() } - backupService.discardSavedBackup() } is BackupAction.CheckForUnfinishedBackup -> { if (backupService.hasIncompletedBackup) { @@ -514,17 +521,21 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } is BackupAction.SkipBackup -> { Analytics.send(Onboarding.Backup.Skipped()) - Analytics.send(Onboarding.Finished()) - finishCardActivation(gatherCardIds(backupState, card)) + + scope.launch { + store.state.globalState.onboardingState.onboardingManager?.finishActivation( + cardIds = gatherCardIds(backupState, card), + ) + } } is BackupAction.FinishBackup -> { - if (action.withAnalytics) { - Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber)) - } + scope.launch { + if (action.withAnalytics) { + Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber)) + } - if (scanResponse != null) { - scope.launch { + if (scanResponse != null) { val userWallet = UserWalletBuilder(scanResponse) .backupCardsIds(backupState.backupCardIds.toSet()) .build() @@ -546,16 +557,22 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) ) store.dispatchOnMain(GlobalAction.UpdateUserWalletsListManager(userWalletsListManager)) } + + val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull { + if (store.state.globalState.onboardingState.onboardingManager?.isActivationFinished(it) == true) { + null + } else { + it + } + } + + // All cardIds may already be activated if the backup was skipped before. + if (notActivatedCardIds.isEmpty()) return@launch + + Analytics.send(Onboarding.Finished()) + + store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds) } - - val notActivatedCardIds = gatherCardIds(backupState, card) - .mapNotNull { if (cardActivationIsFinished(it)) null else it } - - // All cardIds may already be activated if the backup was skipped before. - if (notActivatedCardIds.isEmpty()) return - - Analytics.send(Onboarding.Finished()) - finishCardActivation(notActivatedCardIds) } is BackupAction.ResetBackupCard -> { @@ -566,36 +583,12 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } } -private fun cardActivationIsFinished(cardId: String): Boolean { - return preferencesStorage.usedCardsPrefStorage.isActivationFinished(cardId) -} - -/** - * Standard Wallet cards start activation at OnboardingWalletAction.CreateWallet - */ -private fun startCardActivation(scanResponse: ScanResponse) { - preferencesStorage.usedCardsPrefStorage.activationStarted(scanResponse.card.cardId) -} - -/** - * Standard Wallet cards finish activation at BackupAction.SkipBackup and BackupAction.FinishBackup - */ -internal fun finishCardActivation(cardIds: List) { - cardIds.forEach { cardId -> - preferencesStorage.usedCardsPrefStorage.activationFinished(cardId) - } -} - internal fun gatherCardIds(backupState: BackupState, card: CardDTO?): List { return (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds) .filterNotNull() .distinct() } -private fun finishCardsActivationForDiscardedUnfinishedBackup(cardId: String) { - preferencesStorage.usedCardsPrefStorage.activationFinished(cardId) -} - private fun handleOnBackPressed(state: OnboardingWalletState) { when (state.backupState.backupStep) { BackupStep.InitBackup, BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode, diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index e552d90fd6..eef3877a77 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -8,6 +8,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase +import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase @@ -63,6 +64,7 @@ data class DaggerGraphState( val generalUserWalletsListManager: UserWalletsListManager? = null, val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null, val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, + val cardRepository: CardRepository? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferencesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferencesStoreModule.kt index e36b36096b..bc295ea874 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferencesStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferencesStoreModule.kt @@ -21,7 +21,7 @@ internal object AppPreferencesStoreModule { fun provideAppPreferencesStore( @ApplicationContext appContext: Context, dispatchers: CoroutineDispatcherProvider, - @NetworkMoshi moshi: Moshi, + @SdkMoshi moshi: Moshi, ): AppPreferencesStore { return AppPreferencesStore( preferencesDataStore = PreferencesDataStore.getInstance(context = appContext, dispatcher = dispatchers.io), 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 0c38cd669b..810c38230d 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 @@ -45,7 +45,7 @@ class AppPreferencesStore( * @see getObjectList * */ inline fun MutablePreferences.getObject(key: Preferences.Key): T? { - val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types + val adapter = moshi.adapter(T::class.java) return this[key]?.let(adapter::fromJson) } @@ -55,6 +55,15 @@ class AppPreferencesStore( return this[key]?.let(adapter::fromJson) } + /** Get list of data [T] by string [key] or default */ + inline fun MutablePreferences.getObjectListOrDefault( + key: Preferences.Key, + default: List, + ): List { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + return this[key]?.let(adapter::fromJson) ?: default + } + /** Get map with [String] key and value [V] by string [key] from [MutablePreferences] */ inline fun MutablePreferences.getObjectMap(key: Preferences.Key): Map? { val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt index 84c73a8dc7..e8328a84ea 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt @@ -1,9 +1,11 @@ package com.tangem.data.card +import androidx.datastore.preferences.core.MutablePreferences import com.tangem.datasource.local.card.UsedCardInfo import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList +import com.tangem.datasource.local.preferences.utils.getObjectListSync import com.tangem.domain.card.repository.CardRepository import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow @@ -21,23 +23,73 @@ internal class DefaultCardRepository( } override suspend fun setCardWasScanned(cardId: String) { - appPreferencesStore.editData { mutablePreferences -> - val usedCards: List? = mutablePreferences.getObjectList( - key = PreferencesKeys.USED_CARDS_INFO_KEY, - ) + appPreferencesStore.editUsedCards(cardId) { it.copy(isScanned = true) } + } - val updatedUsedCards = usedCards?.updateCard(cardId) - ?: listOf(UsedCardInfo(cardId = cardId, isScanned = true)) + override suspend fun startCardActivation(cardId: String) { + appPreferencesStore.editUsedCards(cardId) { it.copy(isActivationStarted = true) } + } - mutablePreferences.setObjectList( - key = PreferencesKeys.USED_CARDS_INFO_KEY, - value = updatedUsedCards, - ) + override suspend fun finishCardActivation(cardId: String) { + appPreferencesStore.editUsedCards(cardId) { + it.copy(isActivationStarted = true, isActivationFinished = true) } } - private fun List.updateCard(cardId: String): List { - val card = find { it.cardId == cardId } ?: UsedCardInfo(cardId = cardId, isScanned = true) - return addOrReplace(item = card.copy(isScanned = true), predicate = { it.cardId == cardId }) + override suspend fun finishCardsActivation(cardIds: List) { + appPreferencesStore.editData { mutablePreferences -> + val usedCards = mutablePreferences.getUsedCards() + + val updatedUsedCards = cardIds.map { cardId -> + usedCards.updateCard(cardId) { + it.copy(isActivationStarted = true, isActivationFinished = true) + } + } + + mutablePreferences.setObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards) + } + } + + override suspend fun isActivationStarted(cardId: String): Boolean { + return getUsedCardSync(cardId)?.isActivationStarted ?: false + } + + override suspend fun isActivationFinished(cardId: String): Boolean { + return getUsedCardSync(cardId)?.isActivationFinished ?: false + } + + override suspend fun isActivationInProgress(cardId: String): Boolean { + val card = getUsedCardSync(cardId) ?: return false + + return card.isActivationStarted && !card.isActivationFinished + } + + private suspend fun AppPreferencesStore.editUsedCards(cardId: String, update: (UsedCardInfo) -> UsedCardInfo) { + editData { mutablePreferences -> + val usedCards = mutablePreferences.getUsedCards() + + val updatedUsedCards = usedCards.updateCard(cardId = cardId, update = update) + + mutablePreferences.setObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards) + } + } + + private fun MutablePreferences.getUsedCards(): List { + return with(appPreferencesStore) { + getObjectListOrDefault(key = PreferencesKeys.USED_CARDS_INFO_KEY, default = mutableListOf()) + } + } + + private fun List.updateCard( + cardId: String, + update: (UsedCardInfo) -> UsedCardInfo, + ): List { + val card = find { it.cardId == cardId } ?: UsedCardInfo(cardId = cardId) + return addOrReplace(item = update(card), predicate = { it.cardId == cardId }) + } + + private suspend fun getUsedCardSync(cardId: String): UsedCardInfo? { + return appPreferencesStore.getObjectListSync(PreferencesKeys.USED_CARDS_INFO_KEY) + .firstOrNull { it.cardId == cardId } } } \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt index 91f774282a..4bcd434fe8 100644 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt +++ b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt @@ -3,10 +3,7 @@ package com.tangem.data.source.preferences import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit -import com.tangem.common.json.MoshiJsonConverter -import com.tangem.data.source.preferences.adapters.BigDecimalAdapter import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage -import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage import javax.inject.Inject // 🔥FIXME: Only logic to work with preferences must be here, must be separated to repositories @@ -14,21 +11,13 @@ import javax.inject.Inject @Deprecated("Create repository instead") class PreferencesDataSource @Inject internal constructor(applicationContext: Context) { - val usedCardsPrefStorage: UsedCardsPrefStorage val disclaimerPrefStorage: DisclaimerPrefStorage private val preferences: SharedPreferences = applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) - private val moshiConverter = MoshiJsonConverter( - adapters = listOf(BigDecimalAdapter()) + MoshiJsonConverter.getTangemSdkAdapters(), - typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(), - ) - init { incrementLaunchCounter() - usedCardsPrefStorage = UsedCardsPrefStorage(preferences, moshiConverter) - usedCardsPrefStorage.migrate() disclaimerPrefStorage = DisclaimerPrefStorage(preferences) } diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/model/DataSourceUsedCardInfo.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/model/DataSourceUsedCardInfo.kt deleted file mode 100644 index d05eadc7ca..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/model/DataSourceUsedCardInfo.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.data.source.preferences.model - -internal data class DataSourceUsedCardInfo( - val cardId: String, - val isScanned: Boolean = false, - val isActivationStarted: Boolean = false, - val isActivationFinished: Boolean = false, -) \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/model/DataSourceUsedCardInfoOld.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/model/DataSourceUsedCardInfoOld.kt deleted file mode 100644 index ee01e6752a..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/model/DataSourceUsedCardInfoOld.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.data.source.preferences.model - -internal data class DataSourceUsedCardInfoOld( - val cardId: String, - val isScanned: Boolean = false, - val isActivationStarted: Boolean = false, -) \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/UsedCardsPrefStorage.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/UsedCardsPrefStorage.kt deleted file mode 100644 index de987e84fc..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/UsedCardsPrefStorage.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.tangem.data.source.preferences.storage - -import android.content.SharedPreferences -import androidx.core.content.edit -import com.tangem.common.json.MoshiJsonConverter -import com.tangem.data.source.preferences.model.DataSourceUsedCardInfo -import com.tangem.data.source.preferences.model.DataSourceUsedCardInfoOld - -/** -[REDACTED_AUTHOR] - */ -@Deprecated("Create repository instead") -class UsedCardsPrefStorage internal constructor( - private val preferences: SharedPreferences, - private val jsonConverter: MoshiJsonConverter, -) { - - private val migrationList = mutableListOf( - UserCardInfoToV2(this), - ) - - internal fun migrate() { - migrationList.forEach { it.migrate() } - migrationList.clear() - } - - fun scanned(cardId: String) { - val restoredList = restore() - val foundItem = findCardInfo(cardId, restoredList)?.copy(isScanned = true) - ?: DataSourceUsedCardInfo(cardId, true) - - save(foundItem, restoredList) - } - - fun wasScanned(cardId: String): Boolean { - return findCardInfo(cardId)?.isScanned ?: false - } - - fun activationStarted(cardId: String) { - val restoredList = restore() - val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = true) - ?: DataSourceUsedCardInfo(cardId, isActivationStarted = true) - - save(foundItem, restoredList) - } - - fun activationFinished(cardId: String) { - val restoredList = restore() - var foundItem = findCardInfo(cardId, restoredList) ?: DataSourceUsedCardInfo(cardId) - foundItem = foundItem.copy( - isActivationStarted = true, - isActivationFinished = true, - ) - - save(foundItem, restoredList) - } - - fun isActivationStarted(cardId: String): Boolean { - return findCardInfo(cardId)?.isActivationStarted ?: false - } - - fun isActivationFinished(cardId: String): Boolean { - return findCardInfo(cardId)?.isActivationFinished ?: false - } - - fun isActivationInProgress(cardId: String): Boolean { - val cardInfo = findCardInfo(cardId) ?: return false - return cardInfo.isActivationStarted && !cardInfo.isActivationFinished - } - - fun hadFinishedActivation(): Boolean { - return restore().any { it.isActivationFinished } - } - - private fun findCardInfo( - cardId: String, - list: MutableList? = null, - ): DataSourceUsedCardInfo? { - val findInList = list ?: restore() - return findInList.firstOrNull { it.cardId == cardId } - } - - private fun save(usedCardInfo: DataSourceUsedCardInfo?, usedCardsInfo: MutableList) { - val info = usedCardInfo ?: return - - with(usedCardsInfo) { - val index = indexOfFirst { it.cardId == info.cardId } - if (index == -1) { - add(info) - } else { - set(index, info) - } - } - - save(usedCardsInfo) - } - - private fun save(list: MutableList) { - val json = jsonConverter.toJson(list) - preferences.edit { putString(USED_CARDS_INFO_V2, json) } - } - - private fun restore(): MutableList { - val json = preferences.getString(USED_CARDS_INFO_V2, null) ?: return mutableListOf() - return try { - jsonConverter.fromJson(json, jsonConverter.typedList(DataSourceUsedCardInfo::class.java))!! - } catch (ex: Exception) { - preferences.edit(true) { remove(USED_CARDS_INFO_V2) } - mutableListOf() - } - } - - companion object { - private const val USED_CARDS_INFO_V2 = "usedCardsInfo_v2" - private const val USED_CARDS_INFO = "usedCardsInfo" - } - - private class UserCardInfoToV2( - private val storage: UsedCardsPrefStorage, - ) : Migration { - override fun migrate() { - val restoredCardsInfo = restore() - if (restoredCardsInfo.isEmpty()) return - - val newCardsInfo = restoredCardsInfo.map { cardInfo -> - DataSourceUsedCardInfo( - cardId = cardInfo.cardId, - isScanned = cardInfo.isScanned, - isActivationStarted = true, - isActivationFinished = !cardInfo.isActivationStarted, - ) - }.toMutableList() - storage.save(newCardsInfo) - } - - private fun restore(): MutableList { - val json = storage.preferences.getString(USED_CARDS_INFO, null) ?: return mutableListOf() - return try { - storage.jsonConverter.fromJson( - json, - storage.jsonConverter.typedList(DataSourceUsedCardInfoOld::class.java), - )!! - } catch (ex: Exception) { - mutableListOf() - } finally { - storage.preferences.edit(true) { remove(USED_CARDS_INFO) } - } - } - } -} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt index 27bb1657ab..97a087c85e 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt @@ -7,4 +7,19 @@ interface CardRepository { fun wasCardScanned(cardId: String): Flow suspend fun setCardWasScanned(cardId: String) + + suspend fun startCardActivation(cardId: String) + + suspend fun finishCardActivation(cardId: String) + + suspend fun finishCardsActivation(cardIds: List) + + @Throws + suspend fun isActivationStarted(cardId: String): Boolean + + @Throws + suspend fun isActivationFinished(cardId: String): Boolean + + @Throws + suspend fun isActivationInProgress(cardId: String): Boolean } \ No newline at end of file From fc0a632080a299273c611ea73e19a1af8a5535a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Feb 2024 18:58:12 +0300 Subject: [PATCH 17/33] Updated on 2026-08-14 --- .../viewmodels/AddCustomTokenViewModel.kt | 5 - .../presentation/wallet/ui/WalletScreen.kt | 169 ++++++++++++++---- 2 files changed, 131 insertions(+), 43 deletions(-) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt index 71cb37a5e9..1b107076ea 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse @@ -105,10 +104,6 @@ internal class AddCustomTokenViewModel @Inject constructor( uiState = stateFactory.getInitialState() } - override fun onDestroy(owner: LifecycleOwner) { - uiState = stateFactory.getInitialState() - } - private suspend fun selectSuitableWallet(suitableUserWallets: List): UserWalletId? { val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() val selectedWalletId = if (walletSupportsAddingTokens(selectedWallet) && suitableUserWallets.isNotEmpty()) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index c7913c1fc6..47555b5577 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -23,6 +22,7 @@ import androidx.paging.compose.collectAsLazyPagingItems import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet @@ -104,7 +104,7 @@ private fun WalletContent( bottomSheetContent: @Composable () -> Unit, alertConfig: WalletAlertState?, ) { - var selectedWalletIndex by remember { mutableIntStateOf(state.selectedWalletIndex) } + var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) } val selectedWallet = state.wallets[selectedWalletIndex] val scaffoldContent: @Composable () -> Unit = { @@ -226,7 +226,7 @@ private fun WalletContent( } @Suppress("LongParameterList", "LongMethod") -@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable private fun BaseScaffoldManageTokenRedesign( state: WalletScreenState, @@ -237,41 +237,40 @@ private fun BaseScaffoldManageTokenRedesign( alertConfig: WalletAlertState?, content: @Composable () -> Unit, ) { - val scaffoldState = rememberBottomSheetScaffoldState() + // show the bottom sheet if there is at least one multicurrency wallet + val showManageTokensBottomSheet = remember(state.wallets) { + state.wallets.any { it is WalletState.MultiCurrency } + } + val bottomSheetState = rememberSheetStateEnhanced( + initialValue = if (showManageTokensBottomSheet) SheetValue.PartiallyExpanded else SheetValue.Hidden, + confirmValueChange = { sheetValue -> + when { + sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false + sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false + else -> true + } + }, + skipHiddenState = showManageTokensBottomSheet, + ) + + val keyboardShown = keyboardAsState() + + BottomSheetStateEffects( + bottomSheetState = bottomSheetState, + showManageTokensBottomSheet = showManageTokensBottomSheet, + alertConfig = alertConfig, + keyboardShown = keyboardShown, + ) + + val scaffoldState = rememberBottomSheetScaffoldState( + bottomSheetState = bottomSheetState, + snackbarHostState = snackbarHostState, + ) + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } - val systemUiController = rememberSystemUiController() - val navigationBarColor = TangemTheme.colors.background.primary - val navigationBarColorWithout = TangemTheme.colors.background.secondary - - DisposableEffect( - navigationBarColor, - navigationBarColorWithout, - ) { - systemUiController.setNavigationBarColor(navigationBarColor) - onDispose { - systemUiController.setNavigationBarColor(navigationBarColorWithout) - } - } - - val keyboardShown by keyboardAsState() - // expand bottom sheet when keyboard appears - LaunchedEffect(keyboardShown is Keyboard.Opened) { - if (keyboardShown is Keyboard.Opened && alertConfig == null) { - scaffoldState.bottomSheetState.expand() - } - } - - val keyboardController = LocalSoftwareKeyboardController.current - val sheetHasBeenHidden = scaffoldState.bottomSheetState.targetValue == SheetValue.PartiallyExpanded - // hide keyboard when bottom sheet is about to be hidden - LaunchedEffect(sheetHasBeenHidden) { - if (sheetHasBeenHidden) { - keyboardController?.hide() - } - } - val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight + val coroutineScope = rememberCoroutineScope() BottomSheetScaffold( @@ -301,10 +300,10 @@ private fun BaseScaffoldManageTokenRedesign( // hide bottom sheet when back pressed BackHandler( - keyboardShown is Keyboard.Closed && - scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded, + keyboardShown.value is Keyboard.Closed && + bottomSheetState.currentValue == SheetValue.Expanded, ) { - coroutineScope.launch { scaffoldState.bottomSheetState.partialExpand() } + coroutineScope.launch { bottomSheetState.partialExpand() } } }, content = { paddingValues -> @@ -330,6 +329,100 @@ private fun BaseScaffoldManageTokenRedesign( ) } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun BottomSheetStateEffects( + bottomSheetState: SheetState, + showManageTokensBottomSheet: Boolean, + alertConfig: WalletAlertState?, + keyboardShown: State, +) { + // Bottom sheet during initialization internally expand partially after its content was remeasured, + // therefore initialValue = SheetValue.Hidden in rememberStandardBottomSheetState doesn't work as expected + // so we have to manually restrict expansion in this case + LaunchedEffect(bottomSheetState.targetValue, bottomSheetState.currentValue) { + if (!showManageTokensBottomSheet && + (bottomSheetState.targetValue != SheetValue.Hidden || bottomSheetState.currentValue != SheetValue.Hidden) + ) { + bottomSheetState.hide() + } + } + // react to changes in wallet list + LaunchedEffect(showManageTokensBottomSheet) { + when { + showManageTokensBottomSheet && bottomSheetState.currentValue != SheetValue.PartiallyExpanded -> { + bottomSheetState.partialExpand() + } + !showManageTokensBottomSheet && bottomSheetState.targetValue != SheetValue.Hidden -> { + bottomSheetState.hide() + } + } + } + + val systemUiController = rememberSystemUiController() + val navigationBarColor = TangemTheme.colors.background.primary + val navigationBarColorWithout = TangemTheme.colors.background.secondary + + SystemBarsEffect { + if (showManageTokensBottomSheet) { + setNavigationBarColor(navigationBarColor) + } + } + DisposableEffect( + showManageTokensBottomSheet, + ) { + onDispose { + if (showManageTokensBottomSheet) { + systemUiController.setNavigationBarColor(navigationBarColorWithout) + } + } + } + + // expand bottom sheet when keyboard appears + LaunchedEffect(keyboardShown.value is Keyboard.Opened) { + if (keyboardShown.value is Keyboard.Opened && alertConfig == null) { + bottomSheetState.expand() + } + } + + val keyboardController = LocalSoftwareKeyboardController.current + // hide keyboard when bottom sheet is about to be hidden + LaunchedEffect(Unit) { + snapshotFlow { + bottomSheetState.currentValue == SheetValue.Expanded && + bottomSheetState.targetValue == SheetValue.PartiallyExpanded + }.collect { sheetHasBeenHidden -> + if (sheetHasBeenHidden) { + keyboardController?.hide() + } + } + } +} + +/** + * Use a standard method when this is fixed https://issuetracker.google.com/issues/314796718 + * Current material3 version: 1.2.0 + */ +@Composable +@ExperimentalMaterial3Api +private fun rememberSheetStateEnhanced( + skipPartiallyExpanded: Boolean = false, + confirmValueChange: (SheetValue) -> Boolean = { true }, + initialValue: SheetValue = SheetValue.Hidden, + skipHiddenState: Boolean = false, +): SheetState { + val density = LocalDensity.current + return remember(initialValue, skipPartiallyExpanded, confirmValueChange, skipHiddenState) { + SheetState( + skipPartiallyExpanded = skipPartiallyExpanded, + density = density, + initialValue = initialValue, + confirmValueChange = confirmValueChange, + skipHiddenState = skipHiddenState, + ) + } +} + @OptIn(ExperimentalMaterialApi::class) @Composable private fun BaseScaffold( From 494aa88d354645dc00ecfdc9b10271cc25708896 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 22 Feb 2024 13:38:30 +0300 Subject: [PATCH 18/33] Updated on 2026-08-14 --- .../presentation/wallet/ui/WalletScreen.kt | 66 +++++++++++++++---- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 47555b5577..803458c2fb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -1,7 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.ui import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.TweenSpec +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -14,9 +18,12 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.unit.Dp import androidx.paging.compose.collectAsLazyPagingItems import com.google.accompanist.systemuicontroller.rememberSystemUiController @@ -274,9 +281,6 @@ private fun BaseScaffoldManageTokenRedesign( val coroutineScope = rememberCoroutineScope() BottomSheetScaffold( - topBar = { - WalletTopBar(config = state.topBarConfig) - }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, @@ -312,23 +316,59 @@ private fun BaseScaffoldManageTokenRedesign( onRefresh = selectedWallet.pullToRefreshConfig.onRefresh, ) - Box( - modifier = Modifier - .pullRefresh(pullRefreshState) - .padding(paddingValues), + Column( + modifier = Modifier.padding(paddingValues), ) { - content() + WalletTopBar(config = state.topBarConfig) + Box( + modifier = Modifier.pullRefresh(pullRefreshState), + ) { + content() - WalletPullToRefreshIndicator( - isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) + WalletPullToRefreshIndicator( + isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } } + + BottomSheetScrim( + color = BottomSheetDefaults.ScrimColor, + visible = bottomSheetState.targetValue == SheetValue.Expanded, + onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } }, + ) }, ) } +@Composable +private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) { + val alpha by animateFloatAsState( + targetValue = if (visible) 1f else 0f, + animationSpec = TweenSpec(), + label = "scrim", + ) + val dismissSheet = if (visible) { + Modifier + .pointerInput(onDismissRequest) { + detectTapGestures { + onDismissRequest() + } + } + .clearAndSetSemantics {} + } else { + Modifier + } + Canvas( + Modifier + .fillMaxSize() + .then(dismissSheet), + ) { + drawRect(color = color, alpha = alpha) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun BottomSheetStateEffects( From aa059666ba36db31395ef71b8df77ce046c736fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 Feb 2024 23:53:28 +0700 Subject: [PATCH 19/33] Updated on 2026-08-14 --- .../tap/features/disclaimer/Disclaimer.kt | 18 ++++------ .../disclaimer/DisclaimerDataProvider.kt | 5 ++- .../tap/features/disclaimer/DisclaimerType.kt | 24 ++++++++++--- .../disclaimer/redux/DisclaimerMiddleware.kt | 10 ++++-- .../disclaimer/ui/DisclaimerFragment.kt | 14 +++++--- .../local/preferences/PreferencesKeys.kt | 6 ++++ .../tangem/data/card/DefaultCardRepository.kt | 35 +++++++++++++++++++ .../preferences/PreferencesDataSource.kt | 4 --- .../storage/DisclaimerPrefStorage.kt | 21 ----------- .../domain/card/repository/CardRepository.kt | 10 ++++++ 10 files changed, 96 insertions(+), 51 deletions(-) delete mode 100644 data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/DisclaimerPrefStorage.kt diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/Disclaimer.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/Disclaimer.kt index 6f09751342..f92bc4be2f 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/Disclaimer.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/Disclaimer.kt @@ -8,8 +8,8 @@ import android.net.Uri interface Disclaimer { fun type(): DisclaimerType fun getUri(): Uri - fun accept() - fun isAccepted(): Boolean + suspend fun accept() + suspend fun isAccepted(): Boolean } abstract class BaseDisclaimer( @@ -18,32 +18,28 @@ abstract class BaseDisclaimer( val baseUrl = "https://tangem.com" - override fun accept() { - dataProvider.storage().accept(getPreferenceKey()) + override suspend fun accept() { + dataProvider.accept() } - override fun isAccepted(): Boolean = dataProvider.storage().isAccepted(getPreferenceKey()) - - protected open fun getPreferenceKey(): String = type().name + override suspend fun isAccepted(): Boolean = dataProvider.isAccepted() } class DummyDisclaimer : Disclaimer { override fun type(): DisclaimerType = DisclaimerType.Tangem override fun getUri(): Uri = Uri.parse("https://tangem.com/tangem_tos.html") - override fun accept() {} - override fun isAccepted(): Boolean = false + override suspend fun accept() {} + override suspend fun isAccepted(): Boolean = false } class TangemDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) { override fun type(): DisclaimerType = DisclaimerType.Tangem override fun getUri(): Uri = Uri.parse("$baseUrl/tangem_tos.html") - override fun getPreferenceKey(): String = "tangem_tos_accepted" } class Start2CoinDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) { override fun type(): DisclaimerType = DisclaimerType.Start2Coin override fun getUri(): Uri = Uri.parse("$baseUrl/" + filename(dataProvider.getLanguage(), getRegion())) - override fun getPreferenceKey(): String = "start2Coin_tos_accepted_${getRegion()}" @Suppress("ComplexMethod") private fun filename(languageCode: String, regionCode: String?): String { diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerDataProvider.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerDataProvider.kt index 9d3b4aeda6..ba3c82563d 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerDataProvider.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerDataProvider.kt @@ -1,12 +1,11 @@ package com.tangem.tap.features.disclaimer -import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage - /** [REDACTED_AUTHOR] */ interface DisclaimerDataProvider { fun getLanguage(): String fun getCardId(): String - fun storage(): DisclaimerPrefStorage + suspend fun accept() + suspend fun isAccepted(): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt index b01b2b6f60..152ade0763 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt @@ -2,8 +2,8 @@ package com.tangem.tap.features.disclaimer import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.models.scan.CardDTO -import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage -import com.tangem.tap.preferencesStorage +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.store import java.util.Locale /** @@ -25,7 +25,7 @@ enum class DisclaimerType { } fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer { - val dataProvider = provideDisclaimerDataProvider(cardDTO.cardId) + val dataProvider = provideDisclaimerDataProvider(cardDTO.cardId, this) return when (this) { DisclaimerType.Tangem -> TangemDisclaimer(dataProvider) DisclaimerType.Start2Coin -> Start2CoinDisclaimer(dataProvider) @@ -34,10 +34,24 @@ fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer { fun CardDTO.createDisclaimer(): Disclaimer = DisclaimerType.get(this).createDisclaimer(this) -private fun provideDisclaimerDataProvider(cardId: String): DisclaimerDataProvider { +private fun provideDisclaimerDataProvider(cardId: String, disclaimerType: DisclaimerType): DisclaimerDataProvider { + val cardRepository = store.state.daggerGraphState.get(DaggerGraphState::cardRepository) return object : DisclaimerDataProvider { override fun getLanguage(): String = Locale.getDefault().language override fun getCardId(): String = cardId - override fun storage(): DisclaimerPrefStorage = preferencesStorage.disclaimerPrefStorage + + override suspend fun accept() { + when (disclaimerType) { + DisclaimerType.Tangem -> cardRepository.acceptTangemTOS() + DisclaimerType.Start2Coin -> cardRepository.acceptStart2CoinTOS(cardId) + } + } + + override suspend fun isAccepted(): Boolean { + return when (disclaimerType) { + DisclaimerType.Tangem -> cardRepository.isTangemTOSAccepted() + DisclaimerType.Start2Coin -> cardRepository.isStart2CoinTOSAccepted(cardId) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt index e71184aff2..3e152187ea 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt @@ -3,7 +3,9 @@ package com.tangem.tap.features.disclaimer.redux import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.tap.common.redux.AppState +import com.tangem.tap.mainScope import com.tangem.tap.store +import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware @@ -26,9 +28,11 @@ private fun handleDisclaimerMiddleware(action: Action, appState: AppState) { store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) } is DisclaimerAction.AcceptDisclaimer -> { - state.disclaimer.accept() - store.dispatch(NavigationAction.PopBackTo()) - state.callback?.onAccept?.invoke() + mainScope.launch { + state.disclaimer.accept() + store.dispatch(NavigationAction.PopBackTo()) + state.callback?.onAccept?.invoke() + } } is DisclaimerAction.OnBackPressed -> { store.dispatch(NavigationAction.PopBackTo()) diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index dc438d1872..d6767e13ac 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import android.view.View.OVER_SCROLL_NEVER import android.webkit.WebView +import androidx.lifecycle.lifecycleScope import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding import com.tangem.core.navigation.AppScreen @@ -21,6 +22,7 @@ import com.tangem.tap.features.disclaimer.redux.DisclaimerState import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentDisclaimerBinding +import kotlinx.coroutines.launch import org.rekotlin.StoreSubscriber class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubscriber { @@ -92,10 +94,12 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs store.dispatch(DisclaimerAction.OnBackPressed) } - override fun newState(state: DisclaimerState) = with(binding) { - if (activity == null || view == null) return + override fun newState(state: DisclaimerState) { + return with(binding) { + if (activity == null || view == null) return - updateUiVisibility(state.disclaimer, state.progressState) + updateUiVisibility(state.disclaimer, state.progressState) + } } private fun updateUiVisibility(disclaimer: Disclaimer, progressState: ProgressState?) = with(binding) { @@ -112,7 +116,9 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs groupError.hide() groupLoading.hide() webView.show() - groupAccept.show(!disclaimer.isAccepted()) + lifecycleScope.launch { + groupAccept.show(!disclaimer.isAccepted()) + } } ProgressState.Error -> { root.beginDelayedTransition() 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 2b74eabdad..4ff06e36b2 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 @@ -3,6 +3,7 @@ package com.tangem.datasource.local.preferences import androidx.datastore.preferences.core.* import com.tangem.datasource.local.preferences.PreferencesKeys.APP_LAUNCH_COUNT_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.FUNDS_FOUND_DATE_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY @@ -57,6 +58,10 @@ object PreferencesKeys { val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") } val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") } + + val IS_TANGEM_TOS_ACCEPTED_KEY by lazy { booleanPreferencesKey(name = "tangem_tos_accepted") } + + fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region") } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ @@ -69,6 +74,7 @@ internal fun getTapPrefKeysToMigrate(): Set { USER_WAS_INTERACT_WITH_RATING_KEY, USED_CARDS_INFO_KEY, WAS_TWINS_ONBOARDING_SHOWN, + IS_TANGEM_TOS_ACCEPTED_KEY, ) .map(Preferences.Key<*>::name) .toSet() diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt index e8328a84ea..0d868f3f10 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt @@ -6,6 +6,8 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.datasource.local.preferences.utils.getObjectListSync +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.card.repository.CardRepository import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow @@ -64,6 +66,28 @@ internal class DefaultCardRepository( return card.isActivationStarted && !card.isActivationFinished } + override suspend fun isTangemTOSAccepted(): Boolean { + return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY, default = false) + } + + override suspend fun isStart2CoinTOSAccepted(cardId: String): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.getStart2CoinTOSAcceptedKey(region = getRegion(cardId)), + default = false, + ) + } + + override suspend fun acceptTangemTOS() { + return appPreferencesStore.store(key = PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY, true) + } + + override suspend fun acceptStart2CoinTOS(cardId: String) { + appPreferencesStore.store( + key = PreferencesKeys.getStart2CoinTOSAcceptedKey(region = getRegion(cardId)), + value = true, + ) + } + private suspend fun AppPreferencesStore.editUsedCards(cardId: String, update: (UsedCardInfo) -> UsedCardInfo) { editData { mutablePreferences -> val usedCards = mutablePreferences.getUsedCards() @@ -92,4 +116,15 @@ internal class DefaultCardRepository( return appPreferencesStore.getObjectListSync(PreferencesKeys.USED_CARDS_INFO_KEY) .firstOrNull { it.cardId == cardId } } + + private fun getRegion(cardId: String): String? { + if (cardId.isEmpty()) return null + + return when (cardId[1]) { + '0' -> "fr" + '1' -> "ch" + '2' -> "at" + else -> null + } + } } \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt index 4bcd434fe8..e12d679fd7 100644 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt +++ b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt @@ -3,7 +3,6 @@ package com.tangem.data.source.preferences import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit -import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage import javax.inject.Inject // 🔥FIXME: Only logic to work with preferences must be here, must be separated to repositories @@ -11,14 +10,11 @@ import javax.inject.Inject @Deprecated("Create repository instead") class PreferencesDataSource @Inject internal constructor(applicationContext: Context) { - val disclaimerPrefStorage: DisclaimerPrefStorage - private val preferences: SharedPreferences = applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) init { incrementLaunchCounter() - disclaimerPrefStorage = DisclaimerPrefStorage(preferences) } var shouldShowSaveUserWalletScreen: Boolean diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/DisclaimerPrefStorage.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/DisclaimerPrefStorage.kt deleted file mode 100644 index e91fb9e27c..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/DisclaimerPrefStorage.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.data.source.preferences.storage - -import android.content.SharedPreferences -import androidx.core.content.edit - -/** -[REDACTED_AUTHOR] - */ -@Deprecated("Create repository instead") -class DisclaimerPrefStorage internal constructor( - private val preferences: SharedPreferences, -) { - - fun accept(disclaimerKey: String) { - preferences.edit { putBoolean(disclaimerKey, true) } - } - - fun isAccepted(disclaimerKey: String): Boolean { - return preferences.getBoolean(disclaimerKey, false) - } -} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt index 97a087c85e..68e476443e 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt @@ -22,4 +22,14 @@ interface CardRepository { @Throws suspend fun isActivationInProgress(cardId: String): Boolean + + @Throws + suspend fun isTangemTOSAccepted(): Boolean + + @Throws + suspend fun isStart2CoinTOSAccepted(cardId: String): Boolean + + suspend fun acceptTangemTOS() + + suspend fun acceptStart2CoinTOS(cardId: String) } \ No newline at end of file From 21b711adc64c2f383f3fd22af00a082d333abe2d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 26 Feb 2024 11:39:07 +0500 Subject: [PATCH 20/33] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 4 ++- core/res/src/main/res/values/strings.xml | 2 ++ .../fee/custom/EthereumCustomFeeConverter.kt | 26 ++++++++++++++++--- .../fields/SendAmountFieldChangeConverter.kt | 7 +++++ .../state/fields/SendAmountFieldConverter.kt | 4 ++- .../SendAmountFieldMaxAmountConverter.kt | 8 ++++++ 6 files changed, 45 insertions(+), 6 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index ffb87108d1..7ed29ac733 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -91,7 +91,7 @@ Посмотреть историю транзакций Обозреватель Комиссия - Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузка на сеть, объема транзакции и приоритета исполнения. %s + Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s Подробнее Свое Быстро @@ -495,6 +495,8 @@ Сумма отправки не может быть менее %1$s Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции Возможны задержки по транзакции + Уменьшить до [%s] + Уменьшить на [%s] Необязательное Последние Получатель diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 23e26fd59a..6189bc5cbf 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -507,6 +507,8 @@ Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Existential deposit The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s. + Reduce by [%s] + Reduce to [%s] Optional Please align your QR code with the square to scan it. Ensure you scan %s network address. Recent 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 4be7b3acfb..bf12d6f398 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 @@ -5,6 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.blockchain.common.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 @@ -31,9 +32,10 @@ internal class EthereumCustomFeeConverter( ) : Converter> { override fun convert(value: Fee.Ethereum): ImmutableList { + val feeValue = value.amount.value return persistentListOf( SendTextField.CustomFee( - value = value.amount.value?.parseBigDecimal(value.amount.decimals).orEmpty(), + value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), decimals = value.amount.decimals, symbol = value.amount.currencySymbol, onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT, it) }, @@ -43,7 +45,7 @@ internal class EthereumCustomFeeConverter( ), title = resourceReference(R.string.send_max_fee), footer = resourceReference(R.string.send_max_fee_footer), - label = getFeeFormatted(value.amount.value), + label = getFeeFormatted(feeValue), keyboardActions = KeyboardActions(), ), SendTextField.CustomFee( @@ -67,7 +69,7 @@ internal class EthereumCustomFeeConverter( footer = resourceReference(R.string.send_gas_limit_footer), onValueChange = { clickIntents.onCustomFeeValueChange(GAS_LIMIT, it) }, keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done, + imeAction = if (checkExceedBalance(feeValue)) ImeAction.None else ImeAction.Done, keyboardType = KeyboardType.Number, ), keyboardActions = KeyboardActions(onDone = { clickIntents.onNextClick() }), @@ -133,7 +135,16 @@ internal class EthereumCustomFeeConverter( label = getFeeFormatted(newFeeAmount), ), ) - set(index, this[index].copy(value = value)) + set( + index, + this[index].copy( + value = value, + keyboardOptions = KeyboardOptions( + imeAction = if (!checkExceedBalance(newFeeAmount)) ImeAction.None else ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + ), + ) } } }.toImmutableList() @@ -152,6 +163,13 @@ internal class EthereumCustomFeeConverter( ) } + private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + + return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount + } + companion object { private const val ETHEREUM_GAS_UNIT = "GWEI" private const val ETHEREUM_GAS_DECIMALS = 18 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 b9bfa076bd..630a026952 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 @@ -1,5 +1,8 @@ package com.tangem.features.send.impl.presentation.state.fields +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.extensions.isZero import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal @@ -43,6 +46,10 @@ internal class SendAmountFieldChangeConverter( isError = isExceedBalance, cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = if (!isExceedBalance) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), ), ), feeState = feeState.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt index 83701146e2..ff622738f7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.blockchain.extensions.toBigDecimalOrDefault +import com.tangem.common.extensions.isZero import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -36,12 +37,13 @@ internal class SendAmountFieldConverter( val fiatDecimal = cryptoCurrencyStatus.value.fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO fiatDecimal.parseBigDecimal(FIAT_DECIMALS) } + val isDoneActionEnabled = !cryptoDecimal.isZero() return SendTextField.AmountField( value = value, fiatValue = fiatValue, onValueChange = clickIntents::onAmountValueChange, keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done, + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, keyboardType = KeyboardType.Number, ), keyboardActions = KeyboardActions(onDone = { clickIntents.onNextClick() }), 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 1a9a4d6def..f6875731d7 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 @@ -1,5 +1,8 @@ package com.tangem.features.send.impl.presentation.state.fields +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState @@ -26,6 +29,7 @@ internal class SendAmountFieldMaxAmountConverter( if (decimalCryptoValue.isNullOrZero()) return state + val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero() val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty() val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals).orEmpty() return state.copy( @@ -37,6 +41,10 @@ internal class SendAmountFieldMaxAmountConverter( isError = false, cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), ), ), feeState = feeState.copy( From b130d740093c79d3d9b54adae965923207b07a79 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 21 Feb 2024 15:46:09 +0700 Subject: [PATCH 21/33] Updated on 2026-08-14 --- .../tap/common/extensions/Navigation.kt | 21 ++++++------------- .../tap/common/extensions/WalletManager.kt | 2 +- .../AccessCodeRequestPolicyMiddleware.kt | 3 ++- .../common/redux/global/GlobalMiddleware.kt | 6 +++--- .../com/tangem/tap/domain/TapWalletManager.kt | 5 +++-- .../scanCard/DefaultScanCardProcessor.kt | 5 ++--- .../domain/scanCard/UseCaseScanProcessor.kt | 5 +++-- .../walletconnect/WalletConnectSdkHelper.kt | 8 +++---- .../domain/DefaultCustomTokenInteractor.kt | 5 +++-- .../details/redux/DetailsMiddleware.kt | 16 +++++++------- .../features/details/redux/DetailsReducer.kt | 10 +++++---- .../walletconnect/WalletConnectMiddleware.kt | 16 +++++++------- .../tap/features/disclaimer/DisclaimerType.kt | 3 ++- .../tap/features/home/redux/HomeMiddleware.kt | 11 ++++------ .../features/onboarding/OnboardingHelper.kt | 7 ++----- .../redux/OnboardingWalletMiddleware.kt | 4 ++-- .../send/redux/middlewares/SendMiddleware.kt | 7 ++----- .../viewmodels/TokensListMigration.kt | 5 +++-- .../middlewares/TradeCryptoMiddleware.kt | 13 ++++-------- .../welcome/redux/WelcomeMiddleware.kt | 7 ++++--- .../CurrencyExchangeManager.kt | 3 ++- .../tap/proxy/redux/DaggerGraphState.kt | 9 +------- 22 files changed, 73 insertions(+), 98 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index fa0d174bf9..019b203988 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -128,18 +128,13 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.OnboardingTwins -> OnboardingTwinsFragment() AppScreen.OnboardingOther -> OnboardingOtherCardsFragment() AppScreen.Wallet -> { - store.state.daggerGraphState - .get(getDependency = DaggerGraphState::walletRouter) - .getEntryFragment() + store.inject(getDependency = DaggerGraphState::walletRouter).getEntryFragment() } AppScreen.Send -> { - val featureToggles = store.state.daggerGraphState.get( - getDependency = DaggerGraphState::sendFeatureToggles, - ) + val featureToggles = store.inject(getDependency = DaggerGraphState::sendFeatureToggles) + if (featureToggles.isRedesignedSendEnabled) { - store.state.daggerGraphState - .get(getDependency = DaggerGraphState::sendRouter) - .getEntryFragment() + store.inject(getDependency = DaggerGraphState::sendRouter).getEntryFragment() } else { SendFragment() } @@ -154,15 +149,11 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.ManageTokens -> TokensListFragment() AppScreen.AddCustomToken -> AddCustomTokenFragment() AppScreen.WalletDetails -> { - store.state.daggerGraphState - .get(getDependency = DaggerGraphState::tokenDetailsRouter) - .getEntryFragment() + store.inject(getDependency = DaggerGraphState::tokenDetailsRouter).getEntryFragment() } AppScreen.WalletConnectSessions -> WalletConnectFragment() AppScreen.QrScanning -> { - store.state.daggerGraphState - .get(getDependency = DaggerGraphState::qrScanningRouter) - .getEntryFragment() + store.inject(getDependency = DaggerGraphState::qrScanningRouter).getEntryFragment() } AppScreen.ReferralProgram -> ReferralFragment() AppScreen.Swap -> SwapFragment() diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 9f6ba786f0..11c26259ee 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -37,7 +37,7 @@ suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result = try } catch (exception: Exception) { Timber.e(exception) - val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager) + val networkConnectionManager = store.inject(DaggerGraphState::networkConnectionManager) if (!networkConnectionManager.isOnline) { Result.Failure(TapError.NoInternetConnection) } else { diff --git a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt index c385b121ae..c8fa6db166 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.redux import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState @@ -20,7 +21,7 @@ class AccessCodeRequestPolicyMiddleware { } private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) { - store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet, ) } 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 666a16ab82..79d6ed3a69 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 @@ -123,7 +123,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) { primaryRules = CardExchangeRules(cardProvider), ) // TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance) - store.state.daggerGraphState.get(DaggerGraphState::appStateHolder).exchangeService = exchangeManager + store.inject(DaggerGraphState::appStateHolder).exchangeService = exchangeManager store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager)) store.dispatchOnMain(GlobalAction.ExchangeManager.Update) } @@ -155,7 +155,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) { } } is GlobalAction.UpdateUserWalletsListManager -> { - val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade) + val walletManagersFacade = store.inject(DaggerGraphState::walletManagersFacade) /* * If implementation of the UserWalletsListManager is changed, @@ -186,7 +186,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) { private fun restoreAppCurrency() { scope.launch { - val currency = store.state.daggerGraphState.get(DaggerGraphState::appCurrencyRepository) + val currency = store.inject(DaggerGraphState::appCurrencyRepository) .getSelectedAppCurrency() .firstOrNull() ?: AppCurrency.Default diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 24db8bcbc8..03f3022a51 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -12,6 +12,7 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet import com.tangem.operations.attestation.Attestation +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -44,8 +45,8 @@ class TapWalletManager( val walletManagerFactory: WalletManagerFactory by lazy { WalletManagerFactory( config = blockchainSdkConfig, - accountCreator = store.state.daggerGraphState.get(DaggerGraphState::accountCreator), - blockchainDataStorage = store.state.daggerGraphState.get(DaggerGraphState::blockchainDataStorage), + accountCreator = store.inject(DaggerGraphState::accountCreator), + blockchainDataStorage = store.inject(DaggerGraphState::blockchainDataStorage), ) } 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 7c21a8a2f2..1bac186d83 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 @@ -5,15 +5,14 @@ import com.tangem.common.core.TangemError import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.common.extensions.inject import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store // TODO: Remove this object after feature toggle was removed and use ScanCardUseCase instead internal class DefaultScanCardProcessor : ScanCardProcessor { private val isNewCardScanningEnabled: Boolean - get() = store.state.daggerGraphState - .get(DaggerGraphState::customTokenFeatureToggles) - .isNewCardScanningEnabled + get() = store.inject(DaggerGraphState::customTokenFeatureToggles).isNewCardScanningEnabled override suspend fun scan( cardId: String?, 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 b5949c7c5a..02e1701894 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 @@ -9,6 +9,7 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.scanCard.chains.* import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter import com.tangem.tap.proxy.redux.DaggerGraphState @@ -23,7 +24,7 @@ internal object UseCaseScanProcessor { cardId: String? = null, allowsRequestAccessCodeFromRepository: Boolean = false, ): CompletionResult { - val scanCardUseCase = store.state.daggerGraphState.get(DaggerGraphState::scanCardUseCase) + val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase) return scanCardUseCase(cardId, allowsRequestAccessCodeFromRepository) .fold( ifLeft = { CompletionResult.Failure(scanCardExceptionConverter.convertBack(it)) }, @@ -44,7 +45,7 @@ internal object UseCaseScanProcessor { ) = progressScope(onProgressStateChange) { onScanStateChange(true) - val scanCardUseCase = store.state.daggerGraphState.get(DaggerGraphState::scanCardUseCase) + val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase) val chains = buildList { add(ScanningFinishedChain { onScanStateChange(false) }) if (analyticsEvent != null) { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index cbc281470e..a1c5cb5c0b 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -20,6 +20,7 @@ import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.operations.sign.SignHashCommand +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder @@ -59,7 +60,7 @@ class WalletConnectSdkHelper { val decimals = wallet.blockchain.decimals() val value = (transaction.value ?: "0").hexToBigDecimal() - ?.movePointLeft(decimals) ?: return null + .movePointLeft(decimals) ?: return null val gasLimit = getGasLimitFromTx(value, walletManager, transaction) @@ -118,8 +119,7 @@ class WalletConnectSdkHelper { private suspend fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? { val userWallet = userWalletsListManager.selectedUserWalletSync ?: return null - val walletManagerFacade = store.state.daggerGraphState - .get(DaggerGraphState::walletManagersFacade) + val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) return walletManagerFacade.getOrCreateWalletManager( userWalletId = userWallet.walletId, blockchain = blockchain, @@ -172,7 +172,7 @@ class WalletConnectSdkHelper { val result = (data.walletManager as TransactionSender).send( transactionData = data.transaction, signer = CommonSigner( - tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk, + tangemSdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk, cardId = cardId, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt index f3f73a98d3..2de3e037af 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -10,6 +10,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.model.Currency import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken import com.tangem.tap.proxy.redux.DaggerGraphState @@ -31,8 +32,8 @@ class DefaultCustomTokenInteractor( // TODO: Move to DI private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) { - val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) - val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository) + val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository) + val networksRepository = store.inject(DaggerGraphState::networksRepository) AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository) } 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 3d68508c7e..0d40b9bca5 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 @@ -265,7 +265,7 @@ class DetailsMiddleware { } private fun changeAppThemeMode(appThemeMode: AppThemeMode) { - val repository = store.state.daggerGraphState.get(DaggerGraphState::appThemeModeRepository) + val repository = store.inject(DaggerGraphState::appThemeModeRepository) scope.launch { repository.changeAppThemeMode(appThemeMode) @@ -275,7 +275,7 @@ class DetailsMiddleware { } private fun changeBalanceHiding(hideBalance: Boolean) { - val repository = store.state.daggerGraphState.get(DaggerGraphState::balanceHidingRepository) + val repository = store.inject(DaggerGraphState::balanceHidingRepository) scope.launch { val newState = repository.getBalanceHidingSettings().copy( @@ -458,8 +458,7 @@ class DetailsMiddleware { Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On)) preferencesStorage.shouldSaveAccessCodes = true - store.state.daggerGraphState - .get(DaggerGraphState::cardSdkConfigRepository) + store.inject(DaggerGraphState::cardSdkConfigRepository) .setAccessCodeRequestPolicy(isBiometricsRequestPolicy = scanResponse?.card?.isAccessCodeSet == true) return CompletionResult.Success(Unit) @@ -471,8 +470,7 @@ class DetailsMiddleware { Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off)) preferencesStorage.shouldSaveAccessCodes = false - store.state.daggerGraphState - .get(DaggerGraphState::cardSdkConfigRepository) + store.inject(DaggerGraphState::cardSdkConfigRepository) .setAccessCodeRequestPolicy(isBiometricsRequestPolicy = false) } .doOnFailure { error -> @@ -531,7 +529,7 @@ class DetailsMiddleware { } private fun scanCard(state: DetailsState) = scope.launch { - store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) + store.inject(DaggerGraphState::scanCardProcessor) .scan(allowsRequestAccessCodeFromRepository = true) .doOnSuccess { scanResponse -> // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards @@ -561,7 +559,7 @@ class DetailsMiddleware { } private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) { - val cardSdkConfigRepository = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) + val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository) val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy() @@ -570,7 +568,7 @@ class DetailsMiddleware { isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, ) - store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( + store.inject(DaggerGraphState::scanCardProcessor).scan( analyticsEvent = Basic.CardWasScanned(CoreAnalyticsParam.ScannedFrom.MyWallets), onWalletNotCreated = { // No need to rollback policy, continue with the policy set before the card scan diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index e6098d1dc9..e829d9c937 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -4,9 +4,11 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.extensions.signedHashesCount import com.tangem.tap.preferencesStorage +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.flow.firstOrNull @@ -76,12 +78,12 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta saveAccessCodes = preferencesStorage.shouldSaveAccessCodes, selectedAppCurrency = store.state.globalState.appCurrency, selectedThemeMode = runBlocking { - store.state.daggerGraphState - .get { appThemeModeRepository }.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT + store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull() + ?: AppThemeMode.DEFAULT }, isHidingEnabled = runBlocking { - store.state.daggerGraphState - .get { balanceHidingRepository }.getBalanceHidingSettings().isHidingEnabledInSettings + store.inject(DaggerGraphState::balanceHidingRepository) + .getBalanceHidingSettings().isHidingEnabledInSettings }, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 60c2e73559..c6422a0706 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -20,6 +20,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.qrscanning.SourceType import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.walletconnect.BnbHelper @@ -47,9 +48,9 @@ import timber.log.Timber class WalletConnectMiddleware { private var walletConnectManager = WalletConnectManager() private val walletConnectInteractor: WalletConnectInteractor - get() = store.state.daggerGraphState.get(DaggerGraphState::walletConnectInteractor) + get() = store.inject(DaggerGraphState::walletConnectInteractor) private val walletConnectRepository: WalletConnectRepository - get() = store.state.daggerGraphState.get(DaggerGraphState::walletConnectRepository) + get() = store.inject(DaggerGraphState::walletConnectRepository) val walletConnectMiddleware: Middleware = { dispatch, state -> { next -> @@ -387,8 +388,7 @@ class WalletConnectMiddleware { } private suspend fun getWalletManagers(): List { - val walletManagerFacade = store.state.daggerGraphState - .get(DaggerGraphState::walletManagersFacade) + val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList() return walletManagerFacade.getStoredWalletManagers(userWallet.walletId) @@ -407,7 +407,7 @@ class WalletConnectMiddleware { } private suspend fun getAvailableEvmBlockchains(userWalletId: UserWalletId): List { - val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) + val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository) return currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) .asSequence() @@ -496,8 +496,7 @@ class WalletConnectMiddleware { style = userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), )?.rawPath - val walletManagerFacade = store.state.daggerGraphState - .get(DaggerGraphState::walletManagersFacade) + val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) return walletManagerFacade.getOrCreateWalletManager( userWalletId = userWallet.walletId, @@ -511,8 +510,7 @@ class WalletConnectMiddleware { } private suspend fun getAccountsForWc(wcInteractor: WalletConnectInteractor, userWallet: UserWallet): List { - val walletManagerFacade = store.state.daggerGraphState - .get(DaggerGraphState::walletManagersFacade) + val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) return walletManagerFacade.getStoredWalletManagers(userWallet.walletId).mapNotNull { val wallet = it.wallet val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull( diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt index 152ade0763..796452dcab 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.disclaimer import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.models.scan.CardDTO +import com.tangem.tap.common.extensions.inject import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import java.util.Locale @@ -35,7 +36,7 @@ fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer { fun CardDTO.createDisclaimer(): Disclaimer = DisclaimerType.get(this).createDisclaimer(this) private fun provideDisclaimerDataProvider(cardId: String, disclaimerType: DisclaimerType): DisclaimerDataProvider { - val cardRepository = store.state.daggerGraphState.get(DaggerGraphState::cardRepository) + val cardRepository = store.inject(DaggerGraphState::cardRepository) return object : DisclaimerDataProvider { override fun getLanguage(): String = Locale.getDefault().language override fun getCardId(): String = cardId 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 d1210fe911..2bec7b7af6 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 @@ -6,18 +6,15 @@ 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.Basic import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletBuilder -import com.tangem.core.analytics.models.Basic import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.Shop import com.tangem.tap.common.entities.IndeterminateProgressButton -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchOpenUrl -import com.tangem.tap.common.extensions.eraseContext -import com.tangem.tap.common.extensions.onUserWalletSelected +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL @@ -79,11 +76,11 @@ private fun handleHomeAction(action: Action) { } private suspend fun readCard(analyticsEvent: AnalyticsEvent?) { - store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, ) - store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( + store.inject(DaggerGraphState::scanCardProcessor).scan( analyticsEvent = analyticsEvent, onProgressStateChange = { showProgress -> if (showProgress) { 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 0d1788de88..fd59efff07 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 @@ -15,10 +15,7 @@ import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.userwallets.UserWalletIdBuilder 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.onUserWalletSelected -import com.tangem.tap.common.extensions.removeContext -import com.tangem.tap.common.extensions.setContext +import com.tangem.tap.common.extensions.* import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.delay @@ -80,7 +77,7 @@ object OnboardingHelper { scope.launch { when { // When should save user wallets, then save card without navigate to save wallet screen - store.state.daggerGraphState.get(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> { + store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> { proceedWithScanResponse(scanResponse, backupCardsIds) store.dispatchOnMain( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index ca0b30020d..460734903f 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -28,6 +28,7 @@ import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse @@ -403,8 +404,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(true)) backupService.addBackupCard { result -> backupService.skipCompatibilityChecks = false - store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk - .config.filter.cardIdFilter = null + store.inject(DaggerGraphState::cardSdkConfigRepository).sdk.config.filter.cardIdFilter = null store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(false)) when (result) { is CompletionResult.Success -> { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 2c21ea333c..dd10c08389 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -24,10 +24,7 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.analytics.events.Token.Send.SelectedCurrency.CurrencyType -import com.tangem.tap.common.extensions.dispatchDialogShow -import com.tangem.tap.common.extensions.dispatchErrorNotification -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.stripZeroPlainString +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -211,7 +208,7 @@ private fun sendTransaction( // return@launch // } - val tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk + val tangemSdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk val linkedTerminalState = tangemSdk.config.linkedTerminal if (card.isStart2Coin) { tangemSdk.config.linkedTerminal = false diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt index 0768165d5b..3a49bac80a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -16,6 +16,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.inject import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import timber.log.Timber @@ -140,8 +141,8 @@ internal class TokensListMigration( private suspend fun removeCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List) { if (currencies.isEmpty()) return - val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) - val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade) + val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository) + val walletManagersFacade = store.inject(DaggerGraphState::walletManagersFacade) currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index a3425aeb83..28a73436b5 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -47,9 +47,7 @@ object TradeCryptoMiddleware { } private val isSendRedesignedEnabled: Boolean - get() = store.state.daggerGraphState.get( - getDependency = DaggerGraphState::sendFeatureToggles, - ).isRedesignedSendEnabled + get() = store.inject(getDependency = DaggerGraphState::sendFeatureToggles).isRedesignedSendEnabled @Suppress("LongMethod", "CyclomaticComplexMethod") private fun handle(state: () -> AppState?, action: TradeCryptoAction) { @@ -106,8 +104,7 @@ object TradeCryptoMiddleware { if (currency is CryptoCurrency.Token && currency.network.isTestnet) { scope.launch { - val walletManager = store.state.daggerGraphState - .get(DaggerGraphState::walletManagersFacade) + val walletManager = store.inject(DaggerGraphState::walletManagersFacade) .getOrCreateWalletManager( userWalletId = action.userWallet.walletId, blockchain = blockchain, @@ -175,8 +172,7 @@ object TradeCryptoMiddleware { val blockchain = Blockchain.fromId(currency.network.id.value) scope.launch { - val walletManager = store.state.daggerGraphState - .get(DaggerGraphState::walletManagersFacade) + val walletManager = store.inject(DaggerGraphState::walletManagersFacade) .getOrCreateWalletManager( userWalletId = action.userWallet.walletId, blockchain = blockchain, @@ -234,8 +230,7 @@ object TradeCryptoMiddleware { val blockchain = Blockchain.fromId(currency.network.id.value) scope.launch { - val walletManager = store.state.daggerGraphState - .get(DaggerGraphState::walletManagersFacade) + val walletManager = store.inject(DaggerGraphState::walletManagersFacade) .getOrCreateWalletManager( userWalletId = action.userWallet.walletId, blockchain = blockchain, 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 555f73988c..5e03eed1a5 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 @@ -8,6 +8,7 @@ import com.tangem.common.doOnSuccess import com.tangem.common.flatMap 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 import com.tangem.domain.common.util.cardTypesResolver @@ -16,9 +17,9 @@ import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.tap.* import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter -import com.tangem.core.analytics.models.Basic 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.redux.AppState import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler @@ -169,10 +170,10 @@ internal class WelcomeMiddleware { } private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) { - store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, ) - store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( + store.inject(DaggerGraphState::scanCardProcessor).scan( analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.SignIn), onSuccess = { scanResponse -> scope.launch { onCardScanned(scanResponse) } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index 7b0d4d5a93..1f0dc4ea68 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -8,6 +8,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.domain.models.scan.CardDTO +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.redux.global.GlobalAction @@ -105,7 +106,7 @@ suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletMa val signer = TangemSigner( card = card, - tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk, + tangemSdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk, initialMessage = Message(), ) { signResponse -> store.dispatch( diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index eef3877a77..c2b7b52edd 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -65,11 +65,4 @@ data class DaggerGraphState( val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null, val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, val cardRepository: CardRepository? = null, -) : StateType { - - inline fun get(getDependency: DaggerGraphState.() -> T?): T { - return requireNotNull(getDependency()) { - "${T::class.simpleName} isn't initialized " - } - } -} \ No newline at end of file +) : StateType \ No newline at end of file From f98622acab217e44d06045b9e054211a04eb1cba Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 26 Feb 2024 15:43:49 +0500 Subject: [PATCH 22/33] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 6 +- .../utils/SendOnNextScreenAnalyticSender.kt | 3 - .../impl/presentation/state/SendAlertState.kt | 11 ++- .../state/SendEventStateFactory.kt | 14 +++ .../presentation/state/SendNotification.kt | 11 +++ .../state/SendNotificationFactory.kt | 33 ++++++- .../presentation/state/SendStateFactory.kt | 7 ++ .../impl/presentation/state/SendUiState.kt | 6 +- .../impl/presentation/state/StateRouter.kt | 10 +- .../presentation/state/fee/FeeCalculation.kt | 14 +-- .../state/fee/FeeNotificationFactory.kt | 16 ---- .../presentation/state/fee/FeeStateFactory.kt | 91 +------------------ .../state/fee/SendFeeNotification.kt | 5 - .../state/fee/SendFeeStateConverter.kt | 6 -- .../fields/SendAmountFieldChangeConverter.kt | 21 ----- .../SendAmountFieldMaxAmountConverter.kt | 4 - .../ui/fee/SendSpeedAndFeeContent.kt | 46 ---------- .../presentation/ui/fee/SendSpeedSelector.kt | 2 +- .../viewmodel/SendClickIntents.kt | 2 +- .../presentation/viewmodel/SendViewModel.kt | 42 ++++++--- .../tangem/feature/swap/ui/StateBuilder.kt | 2 +- 22 files changed, 123 insertions(+), 233 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 7ed29ac733..45b3948014 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -92,7 +92,6 @@ Обозреватель Комиссия Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s - Подробнее Свое Быстро По рынку @@ -111,6 +110,7 @@ OK Основная карта Вставить + Подробнее Получить Отклонить Перезагрузить @@ -457,6 +457,8 @@ Поддержка Транзакция не выполнена Причина: %1$s\nКод: %2$s + Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? + Вычесть %1$s в %2$s Адрес Код назначения diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 6189bc5cbf..246e7b197e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -90,7 +90,6 @@ Explorer Fee Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s - Read more Custom Fast Market @@ -110,6 +109,7 @@ OK Primary Card Paste + Read more Receive Reject Reload @@ -459,6 +459,8 @@ Support The transaction is not completed Reason: %1$s\nCode: %2$s + Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? + Вычесть Confirm %1$s at %2$s Address @@ -486,7 +488,7 @@ Maximum fee amount Numbers only for Destination Tag Network fee - Sending amount will be reduced to cover the selected fee level + Sending amount will be reduced by %1$s to cover the selected commission level. The recipient will get %2$s. Network fee coverage Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendOnNextScreenAnalyticSender.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendOnNextScreenAnalyticSender.kt index e3f5666a1c..4fc8d719af 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendOnNextScreenAnalyticSender.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendOnNextScreenAnalyticSender.kt @@ -23,9 +23,6 @@ internal class SendOnNextScreenAnalyticSender( } analyticsEventHandler.send(SendAnalyticEvents.SelectedFee(selectedFee.name)) } - if (feeState.isSubtract) { - analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount) - } } SendUiStateType.Amount -> { val isFiatSelected = state.amountState?.amountTextField?.isFiatValue ?: return 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 672ceb39d5..ce176c7174 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 @@ -45,11 +45,20 @@ internal sealed class SendAlertState { override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message) } - object FeeIncreased : SendAlertState() { + data object FeeIncreased : SendAlertState() { override val title: TextReference? = null override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title) } + data class FeeCoverage( + override val onConfirmClick: (() -> Unit), + ) : SendAlertState() { + override val title: TextReference? = null + override val message: TextReference = resourceReference(id = R.string.send_alert_fee_coverage_title) + override val confirmButtonText: TextReference = + resourceReference(id = R.string.send_alert_fee_coverage_subract_text) + } + data class ReserveAmount(val amount: String) : SendAlertState() { override val title: TextReference = resourceReference(id = R.string.send_notification_invalid_reserve_amount_title, wrappedList(amount)) 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 23c9aec225..1bfe20a62b 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 @@ -43,6 +43,20 @@ internal class SendEventStateFactory( ) } + fun getFeeCoverageAlert(onConsume: () -> Unit): SendUiState { + val state = currentStateProvider() + return state.copy( + event = triggeredEvent( + data = SendEvent.ShowAlert( + SendAlertState.FeeCoverage( + onConfirmClick = clickIntents::onSubtractSelect, + ), + ), + onConsume = onConsume, + ), + ) + } + fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit, onFeeNotIncreased: () -> Unit): SendUiState { val state = currentStateProvider() val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index cdbd005127..4ab6952836 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -85,5 +85,16 @@ internal sealed class SendNotification(val config: NotificationConfig) { title = resourceReference(R.string.send_notification_existential_deposit_title), subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), ) + + data class NetworkCoverage( + val amountReducedBy: String, + val amountReduced: String, + ) : Warning( + title = resourceReference(id = R.string.send_network_fee_warning_title), + subtitle = resourceReference( + id = R.string.send_network_fee_warning_content, + formatArgs = wrappedList(amountReducedBy, amountReduced), + ), + ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt index cdfb8fc271..6f3021a2b5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt @@ -30,20 +30,22 @@ internal class SendNotificationFactory( .filter { it.type == SendUiStateType.Send } .map { val state = currentStateProvider() + val sendState = state.sendState val feeState = state.feeState ?: return@map persistentListOf() val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO - val amountValue = state.amountState?.amountTextField?.value?.toBigDecimalOrNull() ?: BigDecimal.ZERO - val sendAmount = if (feeState.isSubtract) feeState.receivedAmountValue else amountValue + val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO + val sendAmount = if (sendState.isSubtract) amountValue.minus(feeAmount) else amountValue buildList { // errors addExceedBalanceNotification(feeAmount, sendAmount) - addInvalidAmountNotification(feeState.isSubtract, sendAmount) + addInvalidAmountNotification(sendState.isSubtract, sendAmount) addMinimumAmountErrorNotification(feeAmount, sendAmount) addDustWarningNotification(feeAmount, sendAmount) addTransactionLimitErrorNotification(feeAmount, sendAmount) // warnings + addFeeCoverageNotification(sendState.isSubtract, sendAmount) addExistentialWarningNotification(feeAmount, sendAmount) - addHighFeeWarningNotification(amountValue, state.sendState.ignoreAmountReduce) + addHighFeeWarningNotification(sendAmount, sendState.ignoreAmountReduce) }.toImmutableList() } @@ -234,6 +236,29 @@ internal class SendNotificationFactory( } } + private fun MutableList.addFeeCoverageNotification( + isSubtract: Boolean, + amountValue: BigDecimal, + ) { + val state = currentStateProvider() + val cryptoCurrency = cryptoCurrencyStatusProvider().currency + val feeAmount = state.feeState?.fee?.amount?.value ?: BigDecimal.ZERO + + val amountReducedValue = amountValue.minus(feeAmount) + val amountReducedByValue = amountValue.minus(amountReducedValue) + val amountReducedBy = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = amountReducedByValue, + cryptoCurrency = cryptoCurrency, + ) + val amountReduced = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = amountReducedValue, + cryptoCurrency = cryptoCurrency, + ) + if (isSubtract) { + add(SendNotification.Warning.NetworkCoverage(amountReducedBy, amountReduced)) + } + } + companion object { private const val CARDANO_MINIMUM = "1" private const val DOGECOIN_MINIMUM = "0.01" 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 bbace2d0ac..10b562c4c5 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 @@ -223,6 +223,13 @@ internal class SendStateFactory( //endregion //region send + fun onSubtractSelect(isSubtract: Boolean): SendUiState { + val state = currentStateProvider() + return state.copy( + sendState = state.sendState.copy(isSubtract = isSubtract), + ) + } + fun getSendingStateUpdate(isSending: Boolean): SendUiState { val state = currentStateProvider() return state.copy(sendState = state.sendState.copy(isSending = isSending)) 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 9f1c13016c..8611ded493 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 @@ -75,12 +75,7 @@ internal sealed class SendStates { override val type: SendUiStateType = SendUiStateType.Fee, override val isPrimaryButtonEnabled: Boolean = false, val feeSelectorState: FeeSelectorState, - val isSubtractAvailable: Boolean, - val isSubtract: Boolean, - val isUserSubtracted: Boolean, val fee: Fee?, - val receivedAmountValue: BigDecimal, - val receivedAmount: String, val rate: BigDecimal?, val appCurrency: AppCurrency, val isFeeApproximate: Boolean, @@ -94,6 +89,7 @@ internal sealed class SendStates { override val isPrimaryButtonEnabled: Boolean = true, val isSending: Boolean = false, val isSuccess: Boolean = false, + val isSubtract: Boolean = false, val transactionDate: Long = 0L, val txUrl: String = "", val ignoreAmountReduce: Boolean = false, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index 21a6c3e9db..a9c6451ab9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -119,12 +119,12 @@ internal class StateRouter( mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Fee, isFromConfirmation) } } - private fun continueToSend(show: () -> Unit) { - if (currentState.value.isFromConfirmation) showSend() else show() - } - - private fun showSend() { + fun showSend() { analyticsEventsHandler.send(SendAnalyticEvents.ConfirmationScreenOpened) mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Send, isFromConfirmation = false) } } + + private fun continueToSend(show: () -> Unit) { + if (currentState.value.isFromConfirmation) showSend() else show() + } } \ 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 16d58d39a6..b9a182691f 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 @@ -1,14 +1,14 @@ package com.tangem.features.send.impl.presentation.state.fee -import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState -import java.math.BigDecimal /** - * Calculate receiving amount when fee is subtracted from sending amount + * Check if sending amount with fee is greater than balance */ -internal fun calculateReceiveAmount(state: SendUiState, feeAmount: Fee): BigDecimal { - val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO - val fee = feeAmount.amount.value ?: return BigDecimal.ZERO - return amountValue.minus(fee) +internal fun checkFeeCoverage(state: SendUiState, cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean { + 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 } \ No newline at end of file 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 3581ba0610..47b85049a3 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 @@ -9,7 +9,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.state.SendStates 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.fields.SendTextField @@ -47,7 +46,6 @@ internal class FeeNotificationFactory( val selectedFee = feeSelectorState.selectedFee addTooLowNotification(feeSelectorState.fees, selectedFee, customFee) addTooHighNotification(feeSelectorState.fees, selectedFee, customFee) - addFeeCoverageNotification(feeState, state.amountState) addExceedsBalanceNotification(feeState.fee) } } @@ -89,20 +87,6 @@ internal class FeeNotificationFactory( } } - private fun MutableList.addFeeCoverageNotification( - feeState: SendStates.FeeState, - amountState: SendStates.AmountState?, - ) { - if (!feeState.isSubtractAvailable) return - - val cryptoAmount = coinCryptoCurrencyStatusProvider().value.amount ?: return - val feeValue = feeState.fee?.amount?.value ?: return - val value = amountState?.amountTextField?.cryptoAmount?.value ?: return - if (cryptoAmount <= value + feeValue && feeState.isSubtract && !feeState.isUserSubtracted) { - add(SendFeeNotification.Warning.NetworkCoverage) - } - } - private suspend fun MutableList.addExceedsBalanceNotification(fee: Fee?) { val feeValue = fee?.amount?.value ?: BigDecimal.ZERO val userWalletId = userWalletProvider().walletId 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 b8f99a5511..d928d3c091 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 @@ -3,7 +3,6 @@ package com.tangem.features.send.impl.presentation.state.fee import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -14,7 +13,6 @@ 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 java.math.BigDecimal /** * Factory to produce fee state for [SendUiState] @@ -22,7 +20,6 @@ import java.math.BigDecimal internal class FeeStateFactory( private val clickIntents: SendClickIntents, private val currentStateProvider: Provider, - private val coinCryptoCurrencyStatusProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, private val isFeeApproximateUseCase: IsFeeApproximateUseCase, @@ -55,9 +52,8 @@ internal class FeeStateFactory( ) } - fun onFeeOnLoadedState(fees: TransactionFee, isSubtractAvailable: Boolean): SendUiState { + fun onFeeOnLoadedState(fees: TransactionFee): SendUiState { val state = currentStateProvider() - val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO val feeState = state.feeState ?: return state val feeSelectorState = (feeState.feeSelectorState as? FeeSelectorState.Content)?.copy( fees = fees, @@ -68,43 +64,15 @@ internal class FeeStateFactory( ) val fee = feeConverter.convert(feeSelectorState) - val receivedAmount = calculateReceiveAmount(state, fee) return state.copy( feeState = feeState.copy( - isSubtractAvailable = isSubtractAvailable, feeSelectorState = feeSelectorState, fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - isSubtract = isSubtractAvailable && checkAutoSubtract(state, fee, balance), isFeeApproximate = isFeeApproximate(fee), ), ) } - fun onFeeOnLoadedState(fees: TransactionFee): SendUiState { - val state = currentStateProvider() - val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO - val feeState = state.feeState ?: return state - val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state - - val updatedFeeSelector = feeSelectorState.copy( - fees = fees, - customValues = customFeeFieldConverter.convert(fees.normal), - ) - val fee = feeConverter.convert(updatedFeeSelector) - val receivedAmount = calculateReceiveAmount(state, fee) - return state.copy( - feeState = feeState.copy( - feeSelectorState = updatedFeeSelector, - fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - isSubtract = checkAutoSubtract(state, fee, balance), - ), - ) - } - fun onFeeOnErrorState(): SendUiState { val state = currentStateProvider() return state.copy( @@ -118,18 +86,13 @@ internal class FeeStateFactory( val state = currentStateProvider() val feeState = state.feeState ?: return state val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state - val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType) val fee = feeConverter.convert(updatedFeeSelectorState) - val receivedAmount = calculateReceiveAmount(state, fee) return state.copy( feeState = feeState.copy( fee = fee, feeSelectorState = updatedFeeSelectorState, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - isSubtract = checkAutoSubtract(state, fee, balance), ), ) } @@ -139,34 +102,12 @@ internal class FeeStateFactory( val feeState = state.feeState ?: return state val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state val updatedFeeSelectorState = customFeeFieldConverter.onValueChange(feeSelectorState, index, value) - val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO val fee = feeConverter.convert(updatedFeeSelectorState) - val receivedAmount = calculateReceiveAmount(state, fee) return state.copy( feeState = feeState.copy( feeSelectorState = updatedFeeSelectorState, fee = fee, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - isSubtract = checkAutoSubtract(state, fee, balance), - ), - ) - } - - fun onSubtractSelect(value: Boolean): SendUiState { - val state = currentStateProvider() - val feeState = state.feeState ?: return state - val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state - val fee = feeConverter.convert(feeSelectorState) - val receivedAmount = calculateReceiveAmount(state, fee) - return state.copy( - feeState = feeState.copy( - isSubtract = value, - isUserSubtracted = true, - receivedAmountValue = receivedAmount, - receivedAmount = getFormattedValue(receivedAmount), - fee = fee, ), ) } @@ -187,9 +128,6 @@ internal class FeeStateFactory( ): Boolean { val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return false val customValue = feeSelectorState.customValues.firstOrNull() - val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO - val fee = feeConverter.convert(feeSelectorState) - val feeValue = fee.amount.value ?: BigDecimal.ZERO val isNotCustom = feeSelectorState.selectedFee != FeeType.Custom val isNotEmptyCustom = if (customValue != null) { @@ -198,24 +136,8 @@ internal class FeeStateFactory( false } val noErrors = notifications.none { it is SendFeeNotification.Error } - val isSubtractRequired = when { - !feeState.isSubtractAvailable -> true // current currency is not fee currency - feeValue + feeState.receivedAmountValue >= balance -> feeState.isSubtract - else -> feeValue + feeState.receivedAmountValue <= balance - } - return noErrors && isSubtractRequired && (isNotEmptyCustom || isNotCustom) - } - - private fun checkAutoSubtract(state: SendUiState, fee: Fee, balance: BigDecimal): Boolean { - val feeState = state.feeState ?: return false - val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO - val feeAmount = fee.amount.value ?: BigDecimal.ZERO - return if (feeState.isUserSubtracted) { - feeState.isSubtract - } else { - amountValue + feeAmount >= balance - } + return noErrors && (isNotEmptyCustom || isNotCustom) } private fun isFeeApproximate(fee: Fee): Boolean { @@ -225,13 +147,4 @@ internal class FeeStateFactory( amountType = fee.amount.type, ) } - - private fun getFormattedValue(value: BigDecimal): String { - val cryptoCurrency = cryptoCurrencyStatusProvider().currency - return BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = value, - cryptoCurrency = cryptoCurrency.symbol, - decimals = cryptoCurrency.decimals, - ) - } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt index f895fb4921..56add5cfe5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt @@ -32,11 +32,6 @@ sealed class SendFeeNotification(val config: NotificationConfig) { subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)), ) - object NetworkCoverage : Warning( - title = resourceReference(id = R.string.send_network_fee_warning_title), - subtitle = resourceReference(id = R.string.send_network_fee_warning_content), - ) - data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning( title = resourceReference(R.string.send_fee_unreachable_error_title), subtitle = resourceReference(R.string.send_fee_unreachable_error_text), 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 1dbee6b3fd..2659080a08 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 @@ -6,7 +6,6 @@ import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal internal class SendFeeStateConverter( private val appCurrencyProvider: Provider, @@ -17,12 +16,7 @@ internal class SendFeeStateConverter( val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() return SendStates.FeeState( feeSelectorState = FeeSelectorState.Loading, - isSubtractAvailable = false, - isSubtract = false, - isUserSubtracted = false, fee = null, - receivedAmountValue = BigDecimal.ZERO, - receivedAmount = "", notifications = persistentListOf(), rate = cryptoCurrencyStatus.value.fiatRate, appCurrency = appCurrencyProvider(), 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 630a026952..1b2a4d56f6 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 @@ -6,7 +6,6 @@ import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.extensions.isZero import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.utils.Provider @@ -22,7 +21,6 @@ internal class SendAmountFieldChangeConverter( val state = currentStateProvider() val amountState = state.amountState ?: return state val amountTextField = amountState.amountTextField - val feeState = state.feeState ?: return state if (value.isEmpty()) return state.emptyState() val cryptoDecimals = amountTextField.cryptoAmount.decimals @@ -36,7 +34,6 @@ internal class SendAmountFieldChangeConverter( val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue val isExceedBalance = checkValue.checkExceedBalance(amountTextField) - val isMaxAmount = checkValue.checkMaxAmount(amountTextField) return state.copy( amountState = amountState.copy( isPrimaryButtonEnabled = !isExceedBalance, @@ -52,9 +49,6 @@ internal class SendAmountFieldChangeConverter( ), ), ), - feeState = feeState.copy( - isSubtract = isMaxAmount, - ), ) } @@ -104,19 +98,4 @@ internal class SendAmountFieldChangeConverter( cryptoDecimal > currencyCryptoAmount || cryptoDecimal.isZero() } } - - private fun String.checkMaxAmount(amountTextField: SendTextField.AmountField): Boolean { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - - // If current currency is Token - if (cryptoCurrencyStatus.currency is CryptoCurrency.Token) return false - - val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO - val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO - return if (amountTextField.isFiatValue) { - parseToBigDecimal(amountTextField.fiatAmount.decimals) == currencyFiatAmount - } else { - parseToBigDecimal(amountTextField.cryptoAmount.decimals) == currencyCryptoAmount - } - } } \ 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 f6875731d7..b29459e757 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 @@ -20,7 +20,6 @@ internal class SendAmountFieldMaxAmountConverter( val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val amountState = state.amountState ?: return state val amountTextField = amountState.amountTextField - val feeState = state.feeState ?: return state val cryptoDecimals = amountTextField.cryptoAmount.decimals val fiatDecimals = amountTextField.fiatAmount.decimals @@ -47,9 +46,6 @@ internal class SendAmountFieldMaxAmountConverter( ), ), ), - feeState = feeState.copy( - isSubtract = true, - ), ) } } \ No newline at end of file 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 95a469975a..5acd6b57b1 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 @@ -15,7 +15,6 @@ import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme 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.fee.SendFeeNotification import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList @@ -41,8 +40,6 @@ internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: S topNotifications(notifications) customFee(feeSendState) middleNotifications(notifications) - subtractButton(state, clickIntents) - bottomNotifications(notifications) } } @@ -85,17 +82,6 @@ private fun LazyListScope.middleNotifications( ) } -private fun LazyListScope.bottomNotifications( - configs: ImmutableList, - modifier: Modifier = Modifier, -) { - notifications( - configs = configs.filterIsInstance().toImmutableList(), - isLast = true, - modifier = modifier, - ) -} - @OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.notifications( configs: ImmutableList, @@ -159,36 +145,4 @@ internal fun LazyListScope.customFee(feeSendState: FeeSelectorState, modifier: M } } } -} - -@OptIn(ExperimentalFoundationApi::class) -internal fun LazyListScope.subtractButton( - state: SendStates.FeeState, - clickIntents: SendClickIntents, - modifier: Modifier = Modifier, -) { - val receivedAmount = state.receivedAmount - val isSubtract = state.isSubtract - val isSubtractAvailable = state.isSubtractAvailable - val feeSendState = state.feeSelectorState - if (isSubtractAvailable) { - item { - val feeStateContent = feeSendState as? FeeSelectorState.Content - val isCustomAvailable = feeStateContent?.customValues.isNullOrEmpty().not() - val isCustomSelected = feeStateContent?.selectedFee == FeeType.Custom - val topPadding = if (isCustomSelected && isCustomAvailable) { - TangemTheme.dimens.spacing12 - } else { - TangemTheme.dimens.spacing20 - } - SendSpeedSubtract( - receivingAmount = receivedAmount, - isSubtract = isSubtract, - onSelectClick = clickIntents::onSubtractSelect, - modifier = modifier - .padding(top = topPadding) - .animateItemPlacement(), - ) - } - } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index 611c29dd70..4d13b7d7a5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -154,7 +154,7 @@ internal fun SendSpeedSelector( @Composable private fun FooterText(onReadMoreClick: () -> Unit) { - val linkText = stringResource(R.string.common_fee_selector_link_description) + val linkText = stringResource(R.string.common_read_more) val fullString = stringResource(R.string.common_fee_selector_footer, linkText) val linkTextPosition = fullString.length - linkText.length val defaultStyle = TangemTheme.colors.text.tertiary 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 8d409188fd..6c66189f98 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 @@ -43,7 +43,7 @@ internal interface SendClickIntents { fun onCustomFeeValueChange(index: Int, value: String) - fun onSubtractSelect(value: Boolean) + fun onSubtractSelect() fun onReadMoreClick() // 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 524d95dada..c9969641c5 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 @@ -45,9 +45,9 @@ 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.AmountStateFactory import com.tangem.features.send.impl.presentation.state.fee.FeeNotificationFactory -import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -126,7 +126,6 @@ internal class SendViewModel @Inject constructor( private val feeStateFactory = FeeStateFactory( clickIntents = this, currentStateProvider = Provider { uiState }, - coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), isFeeApproximateUseCase = isFeeApproximateUseCase, @@ -411,6 +410,20 @@ internal class SendViewModel @Inject constructor( override fun popBackStack() = stateRouter.popBackStack() override fun onBackClick() = stateRouter.onBackClick(uiState.sendState.isSuccess) override fun onNextClick() { + val currentState = stateRouter.currentState.value + val isCurrentFee = currentState.type == SendUiStateType.Fee + if (isCurrentFee) { + val isFeeCoverage = checkFeeCoverage(uiState, cryptoCurrencyStatus) + if (isAmountSubtractAvailable && isFeeCoverage) { + uiState = eventStateFactory.getFeeCoverageAlert( + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) + return + } else { + uiState = stateFactory.onSubtractSelect(false) + } + } + val prevScreen = stateRouter.onNextClick() sendOnNextScreenAnalyticSender.send(prevScreen, uiState) } @@ -520,9 +533,9 @@ internal class SendViewModel @Inject constructor( updateFeeNotifications() } - override fun onSubtractSelect(value: Boolean) { - uiState = feeStateFactory.onSubtractSelect(value) - updateFeeNotifications() + override fun onSubtractSelect() { + uiState = stateFactory.onSubtractSelect(true) + stateRouter.showSend() } override fun onReadMoreClick() { @@ -539,12 +552,8 @@ internal class SendViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { uiState = feeStateFactory.onFeeOnLoadingState() uiState = callFeeUseCase()?.fold( - ifRight = { fees -> - feeStateFactory.onFeeOnLoadedState(fees, isAmountSubtractAvailable) - }, - ifLeft = { - feeStateFactory.onFeeOnErrorState() - }, + ifRight = feeStateFactory::onFeeOnLoadedState, + ifLeft = { feeStateFactory.onFeeOnErrorState() }, ) ?: feeStateFactory.onFeeOnErrorState() updateFeeNotifications() }.saveIn(feeJobHolder) @@ -587,16 +596,19 @@ internal class SendViewModel @Inject constructor( } override fun showAmount() { + uiState = stateFactory.onSubtractSelect(false) stateRouter.showAmount(isFromConfirmation = true) analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount)) } override fun showRecipient() { + uiState = stateFactory.onSubtractSelect(false) stateRouter.showRecipient(isFromConfirmation = true) analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address)) } override fun showFee() { + uiState = stateFactory.onSubtractSelect(false) stateRouter.showFee(isFromConfirmation = true) analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) } @@ -623,12 +635,12 @@ internal class SendViewModel @Inject constructor( private fun verifyAndSendTransaction() { val recipient = uiState.recipientState?.addressTextField?.value ?: return val feeState = uiState.feeState ?: return - val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return + val fee = feeState.fee ?: return val memo = uiState.recipientState?.memoTextField?.value - val fee = feeStateFactory.feeConverter.convert(feeSelectorState) val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return - val amountToSend = if (feeState.isSubtract && isAmountSubtractAvailable) { - feeState.receivedAmountValue + val amountToSend = if (uiState.sendState.isSubtract && isAmountSubtractAvailable) { + val feeValue = fee.amount.value ?: return + amountValue.minus(feeValue) } else { amountValue } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 49d98a51dd..0da7c077d1 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1119,7 +1119,7 @@ internal class StateBuilder( }, readMoreUrl = buildReadMoreUrl(), feeItems = txFeeState.toFeeItemState(), - readMore = resourceReference(R.string.common_fee_selector_link_description), + readMore = resourceReference(R.string.common_read_more), onReadMoreClick = actions.onFeeReadMoreClick, ) return uiState.copy( From f1aa5f3a6e0b23c70a7f31f7d0073c702333ade7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Feb 2024 19:58:29 +0100 Subject: [PATCH 23/33] Updated on 2026-08-14 --- .../swap/models/states/ProviderState.kt | 16 +++--- .../swap/ui/ChooseProviderBottomSheet.kt | 4 +- .../tangem/feature/swap/ui/ProviderItem.kt | 55 +++++++++---------- .../tangem/feature/swap/ui/StateBuilder.kt | 32 ++++++----- .../feature/swap/viewmodels/SwapViewModel.kt | 19 ++++--- 5 files changed, 65 insertions(+), 61 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index 265631000e..0e396ca622 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -25,7 +25,7 @@ sealed class ProviderState { val subtitle: TextReference, val selectionType: SelectionType, val additionalBadge: AdditionalBadge, - val percentLowerThenBest: PercentLowerThanBest = PercentLowerThanBest.Empty, + val percentLowerThenBest: PercentDifference = PercentDifference.Empty, override val onProviderClick: (String) -> Unit, ) : ProviderState() @@ -50,9 +50,9 @@ sealed class ProviderState { } } -sealed class PercentLowerThanBest { - data class Value(val value: Float) : PercentLowerThanBest() - object Empty : PercentLowerThanBest() +sealed class PercentDifference { + data class Value(val value: Float) : PercentDifference() + object Empty : PercentDifference() } object ProviderPercentDiffComparator : Comparator { @@ -66,14 +66,14 @@ object ProviderPercentDiffComparator : Comparator { if (o1 is ProviderState.Content && o2 is ProviderState.Content) { val o1Percent = o1.percentLowerThenBest val o2Percent = o2.percentLowerThenBest - if (o1Percent is PercentLowerThanBest.Value && o2Percent !is PercentLowerThanBest.Value) { + if (o1Percent is PercentDifference.Value && o2Percent !is PercentDifference.Value) { return -1 } - if (o1Percent !is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) { + if (o1Percent !is PercentDifference.Value && o2Percent is PercentDifference.Value) { return 1 } - return if (o1Percent is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) { - o1Percent.value.compareTo(o2Percent.value) + return if (o1Percent is PercentDifference.Value && o2Percent is PercentDifference.Value) { + o2Percent.value.compareTo(o1Percent.value) } else { 0 } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 51757b8c16..301815f572 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -19,7 +19,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig -import com.tangem.feature.swap.models.states.PercentLowerThanBest +import com.tangem.feature.swap.models.states.PercentDifference import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.toImmutableList @@ -114,7 +114,7 @@ private fun ChooseProviderBottomSheet_Preview() { iconUrl = "", subtitle = stringReference("1 000 000"), additionalBadge = ProviderState.AdditionalBadge.BestTrade, - percentLowerThenBest = PercentLowerThanBest.Value(-1.0f), + percentLowerThenBest = PercentDifference.Value(-1.0f), selectionType = ProviderState.SelectionType.SELECT, onProviderClick = {}, ), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index eaddec7e0c..34e7da0b20 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -25,7 +25,7 @@ import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.swap.models.states.PercentLowerThanBest +import com.tangem.feature.swap.models.states.PercentDifference import com.tangem.feature.swap.models.states.ProviderState /** @@ -93,11 +93,8 @@ private fun ProviderContentState( .padding(start = TangemTheme.dimens.spacing12) .size(size = TangemTheme.dimens.size40) .clip(TangemTheme.shapes.roundedCorners8), - model = ImageRequest.Builder(context = LocalContext.current) - .data(state.iconUrl) - .crossfade(enable = true) - .allowHardware(false) - .build(), + model = ImageRequest.Builder(context = LocalContext.current).data(state.iconUrl) + .crossfade(enable = true).allowHardware(false).build(), loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, error = { ErrorProviderIcon( @@ -135,10 +132,12 @@ private fun ProviderContentState( ) } when (state.additionalBadge) { - ProviderState.AdditionalBadge.BestTrade -> - BestTradeItem(Modifier.padding(start = TangemTheme.dimens.spacing4)) - ProviderState.AdditionalBadge.PermissionRequired -> - PermissionBadgeItem(Modifier.padding(start = TangemTheme.dimens.spacing4)) + ProviderState.AdditionalBadge.BestTrade -> BestTradeItem( + Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + ProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem( + Modifier.padding(start = TangemTheme.dimens.spacing4), + ) ProviderState.AdditionalBadge.Empty -> { // no-op } @@ -159,14 +158,19 @@ private fun ProviderContentState( maxLines = 1, ) } - if (state.percentLowerThenBest is PercentLowerThanBest.Value && - state.percentLowerThenBest.value > 0 + if (state.percentLowerThenBest is PercentDifference.Value && + state.percentLowerThenBest.value != 0f ) { + val textColor = if (state.percentLowerThenBest.value > 0) { + TangemTheme.colors.icon.accent + } else { + TangemTheme.colors.text.warning + } AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") { Text( - text = "-$it%", + text = if (it > 0) "+$it%" else "$it%", style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.warning, + color = textColor, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), overflow = TextOverflow.Ellipsis, maxLines = 1, @@ -195,11 +199,8 @@ private fun ProviderUnavailableState( .padding(start = TangemTheme.dimens.spacing12) .size(size = TangemTheme.dimens.size40) .clip(TangemTheme.shapes.roundedCorners8), - model = ImageRequest.Builder(context = LocalContext.current) - .data(state.iconUrl) - .crossfade(enable = true) - .allowHardware(false) - .build(), + model = ImageRequest.Builder(context = LocalContext.current).data(state.iconUrl) + .crossfade(enable = true).allowHardware(false).build(), loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, error = { ErrorProviderIcon( @@ -295,8 +296,7 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) { @Composable private fun BoxScope.ProviderChevron(selectionType: ProviderState.SelectionType, isSelected: Boolean) { when (selectionType) { - ProviderState.SelectionType.NONE -> { - /* no-op */ + ProviderState.SelectionType.NONE -> { /* no-op */ } ProviderState.SelectionType.CLICK -> { Icon( @@ -342,11 +342,10 @@ private fun BaseContainer(modifier: Modifier = Modifier, content: @Composable Bo @Composable private fun ErrorProviderIcon(modifier: Modifier = Modifier) { Box( - modifier = modifier - .background( - color = TangemTheme.colors.background.secondary, - shape = TangemTheme.shapes.roundedCorners8, - ), + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCorners8, + ), contentAlignment = Alignment.Center, ) { Icon( @@ -366,7 +365,7 @@ private fun BestTradeItem(modifier: Modifier = Modifier) { ), ) { Text( - text = "Best rate", + text = stringResource(R.string.express_provider_best_rate), style = TangemTheme.typography.caption1, color = TangemTheme.colors.icon.accent, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), @@ -417,7 +416,7 @@ private fun ProviderItem_Content_Preview() { iconUrl = "", subtitle = stringReference("1 000 000"), additionalBadge = ProviderState.AdditionalBadge.PermissionRequired, - percentLowerThenBest = PercentLowerThanBest.Value(-1.0f), + percentLowerThenBest = PercentDifference.Value(12.0f), selectionType = ProviderState.SelectionType.SELECT, onProviderClick = {}, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 0da7c077d1..84aba17a86 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1024,6 +1024,7 @@ internal class StateBuilder( fun showSelectProviderBottomSheet( uiState: SwapStateHolder, selectedProviderId: String, + bestRatedProviderId: String, pricesLowerBest: Map, providersStates: Map, unavailableProviders: List, @@ -1031,7 +1032,7 @@ internal class StateBuilder( ): SwapStateHolder { val availableProvidersStates = providersStates.entries .mapNotNull { - it.convertToProviderBottomSheetState(pricesLowerBest, actions.onProviderSelect) + it.convertToProviderBottomSheetState(pricesLowerBest, bestRatedProviderId, actions.onProviderSelect) } .sortedWith(ProviderPercentDiffComparator) val unavailableProviderStates = unavailableProviders.map { @@ -1072,8 +1073,8 @@ internal class StateBuilder( it.copy( subtitle = stringReference(rateString), percentLowerThenBest = pricesLowerBest[it.id]?.let { percent -> - PercentLowerThanBest.Value(percent) - } ?: PercentLowerThanBest.Empty, + PercentDifference.Value(percent) + } ?: PercentDifference.Value(0f), ) } else { it @@ -1179,18 +1180,21 @@ internal class StateBuilder( private fun Map.Entry.convertToProviderBottomSheetState( pricesLowerBest: Map, + bestRatedProviderId: String, onProviderSelect: (String) -> Unit, ): ProviderState? { val provider = this.key return when (val state = this.value) { is SwapState.EmptyAmountState -> null - is SwapState.QuotesLoadedState -> provider.convertToContentSelectableProviderState( - isBestRate = false, // not show best rate in bottom sheet - state = state, - onProviderClick = onProviderSelect, - pricesLowerBest = pricesLowerBest, - selectionType = ProviderState.SelectionType.SELECT, - ) + is SwapState.QuotesLoadedState -> { + provider.convertToContentSelectableProviderState( + isBestRate = bestRatedProviderId == provider.providerId, + state = state, + onProviderClick = onProviderSelect, + pricesLowerBest = pricesLowerBest, + selectionType = ProviderState.SelectionType.SELECT, + ) + } is SwapState.SwapError -> getProviderStateForError( swapProvider = provider, fromToken = state.fromTokenInfo.cryptoCurrencyStatus.currency, @@ -1282,7 +1286,7 @@ internal class StateBuilder( subtitle = stringReference(rateString), additionalBadge = badge, selectionType = selectionType, - percentLowerThenBest = PercentLowerThanBest.Empty, + percentLowerThenBest = PercentDifference.Empty, onProviderClick = onProviderClick, ) } @@ -1312,8 +1316,8 @@ internal class StateBuilder( additionalBadge = additionalBadge, selectionType = selectionType, percentLowerThenBest = pricesLowerBest[this.providerId]?.let { percent -> - PercentLowerThanBest.Value(percent) - } ?: PercentLowerThanBest.Value(0f), + PercentDifference.Value(percent) + } ?: PercentDifference.Value(0f), onProviderClick = onProviderClick, ) } @@ -1347,7 +1351,7 @@ internal class StateBuilder( selectionType = selectionType, subtitle = alertText, additionalBadge = ProviderState.AdditionalBadge.Empty, - percentLowerThenBest = PercentLowerThanBest.Empty, + percentLowerThenBest = PercentDifference.Empty, onProviderClick = onProviderClick, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index be2e616805..75561ff004 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -49,7 +49,6 @@ import java.text.DecimalFormat import java.text.NumberFormat import java.util.Locale import javax.inject.Inject -import kotlin.math.absoluteValue import kotlin.properties.Delegates typealias SuccessLoadedSwapData = Map @@ -325,7 +324,7 @@ internal class SwapViewModel @Inject constructor( val (provider, state) = updateLoadedQuotes(providersState) setupLoadedState(provider, state, fromToken) val successStates = providersState.getLastLoadedSuccessStates() - val pricesLowerBest = getPricesLowerBest(successStates) + val pricesLowerBest = getPricesLowerBest(provider.providerId, successStates) uiState = stateBuilder.updateProvidersBottomSheetContent( uiState = uiState, pricesLowerBest = pricesLowerBest, @@ -908,13 +907,14 @@ internal class SwapViewModel @Inject constructor( onProviderClick = { providerId -> analyticsEventHandler.send(SwapEvents.ProviderClicked) val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() - val pricesLowerBest = getPricesLowerBest(states) + val pricesLowerBest = getPricesLowerBest(providerId, states) val unavailableProviders = getUnavailableProvidersFor(dataState.lastLoadedSwapStates) uiState = stateBuilder.showSelectProviderBottomSheet( uiState = uiState, selectedProviderId = providerId, pricesLowerBest = pricesLowerBest, unavailableProviders = unavailableProviders, + bestRatedProviderId = findBestQuoteProvider(states)?.providerId ?: providerId, providersStates = dataState.lastLoadedSwapStates, ) { uiState = stateBuilder.dismissBottomSheet(uiState) } }, @@ -1023,17 +1023,18 @@ internal class SwapViewModel @Inject constructor( }?.key } - private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map { - val bestRateEntry = state.maxByOrNull { it.value.toTokenInfo.tokenAmount.value } ?: return emptyMap() - val bestRate = bestRateEntry.value.toTokenInfo.tokenAmount.value + private fun getPricesLowerBest(selectedProviderId: String, state: SuccessLoadedSwapData): Map { + val selectedProviderEntry = state.filter { it.key.providerId == selectedProviderId }.entries.firstOrNull() + ?: return emptyMap() + val selectedProviderRate = selectedProviderEntry.value.toTokenInfo.tokenAmount.value val hundredPercent = BigDecimal("100") return state.entries.mapNotNull { - if (it.key != bestRateEntry.key) { + if (it.key != selectedProviderEntry.key) { val amount = it.value.toTokenInfo.tokenAmount.value val percentDiff = BigDecimal.ONE.minus( - amount.divide(bestRate, RoundingMode.HALF_UP), + selectedProviderRate.divide(amount, RoundingMode.HALF_UP), ).multiply(hundredPercent) - it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat().absoluteValue + it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat() } else { null } From 1aa14238fb51797a530d39841a84d37d6f73f72a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Feb 2024 15:03:42 +0000 Subject: [PATCH 24/33] Updated on 2026-08-14 --- .../tap/features/details/ui/details/DetailsScreen.kt | 6 ++++-- .../features/details/ui/details/DetailsScreenState.kt | 2 +- .../features/details/ui/details/DetailsViewModel.kt | 11 +++-------- .../tap/features/onboarding/OnboardingMenuProvider.kt | 3 ++- .../products/wallet/ui/OnboardingWalletFragment.kt | 3 ++- .../wallet/ui/dialogs/WalletActivationErrorDialog.kt | 5 +++-- .../features/send/ui/dialogs/RequestFeeErrorDialog.kt | 2 +- .../send/ui/dialogs/SendTransactionFailsDialog.kt | 2 +- app/src/main/res/menu/menu_onboarding.xml | 2 +- core/res/src/main/res/values-ru/strings.xml | 4 ++-- core/res/src/main/res/values-zh-rTW/strings.xml | 1 - core/res/src/main/res/values/strings.xml | 4 ++-- .../send/impl/presentation/state/SendAlertState.kt | 4 ++-- 13 files changed, 24 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index 8a65d0e7bd..ec5a34ac4e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -262,7 +262,8 @@ private class DetailsScreenStateProvider : CollectionPreviewParameterProvider Unit, ) : SettingsItem( iconResId = R.drawable.ic_comment, - title = resourceReference(R.string.details_row_title_send_feedback), + title = resourceReference(R.string.details_row_title_contact_to_support), ) data class ReferralProgram( 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 3e12848581..5d4f2b20df 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 @@ -17,7 +17,6 @@ import com.tangem.tap.common.extensions.addContext import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.feedback.FeedbackEmail -import com.tangem.tap.common.feedback.SupportInfo import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.details.redux.DetailsAction @@ -89,8 +88,9 @@ internal class DetailsViewModel( SettingsItem.AppSettings(::navigateToAppSettings) .let(::add) - SettingsItem.Chat(::navigateToChat) - .let(::add) + // removed chat in task [REDACTED_TASK_KEY] + // SettingsItem.Chat(::navigateToChat) + // .let(::add) SettingsItem.SendFeedback(::sendFeedback) .let(::add) @@ -141,11 +141,6 @@ internal class DetailsViewModel( store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) } - private fun navigateToChat() { - Analytics.send(Settings.ButtonChat()) - store.dispatchOnMain(GlobalAction.OpenChat(SupportInfo())) - } - private fun navigateToAppSettings() { Analytics.send(Settings.ButtonAppSettings()) store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.AppSettings)) 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 4af057eb58..b979fd2d8c 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 @@ -22,7 +22,8 @@ class OnboardingMenuProvider : MenuProvider { override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { R.id.menu_item_chat_support -> { Analytics.send(Onboarding.ButtonChat()) - store.dispatch(GlobalAction.OpenChat(SupportInfo())) + // changed on email support [REDACTED_TASK_KEY] + store.dispatch(GlobalAction.SendEmail(SupportInfo())) true } else -> false 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 9a08513cdb..dd07c89355 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 @@ -501,7 +501,8 @@ class OnboardingWalletFragment : private fun makeSeedPhraseRouter(): SeedPhraseRouter = SeedPhraseRouter( onBack = ::legacyOnBackHandler, onOpenChat = { - store.dispatch(GlobalAction.OpenChat(SupportInfo())) + // changed on email support [REDACTED_TASK_KEY] + store.dispatch(GlobalAction.SendEmail(SupportInfo())) }, onOpenUriClick = { uri -> store.dispatchOpenUrl(uri.toString()) 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 bc213e1d41..cfabd25957 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 @@ -17,8 +17,9 @@ object WalletActivationErrorDialog { setTitle(context.getString(R.string.onboarding_activation_error_title)) setMessage(context.getString(R.string.onboarding_activation_error_message)) setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() } - setNegativeButton(R.string.chat_button_title) { _, _ -> - store.dispatch(GlobalAction.OpenChat(SupportInfo())) + setNegativeButton(R.string.common_support) { _, _ -> + // changed on email support [REDACTED_TASK_KEY] + store.dispatch(GlobalAction.SendEmail(SupportInfo())) } setOnDismissListener { store.dispatchDialogHide() } setCancelable(false) 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 8da2d25445..cc67c24921 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 @@ -18,7 +18,7 @@ object RequestFeeErrorDialog { return AlertDialog.Builder(context).apply { setTitle(R.string.common_fee_error) setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) - setNegativeButton(R.string.alert_button_send_feedback) { _, _ -> + setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ -> 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 8d1fe0182f..4c300f7aeb 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 @@ -30,7 +30,7 @@ object SendTransactionFailsDialog { return AlertDialog.Builder(context).apply { setTitle(R.string.alert_failed_to_send_transaction_title) setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) - setNeutralButton(R.string.alert_button_send_feedback) { _, _ -> + setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ -> store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage))) } setPositiveButton(R.string.common_cancel) { _, _ -> } diff --git a/app/src/main/res/menu/menu_onboarding.xml b/app/src/main/res/menu/menu_onboarding.xml index 28e7e73a09..fb7ee134ce 100644 --- a/app/src/main/res/menu/menu_onboarding.xml +++ b/app/src/main/res/menu/menu_onboarding.xml @@ -3,6 +3,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> \ 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 45b3948014..62e755b2c6 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -130,6 +130,7 @@ Начать Отправить Успешно + Поддержка Обмен условия участия Ошибка транзакции @@ -181,11 +182,11 @@ Privacy policy %s хэшей Номер карты + Обратиться в поддержку Добавить еще карты Валюта приложения Скрывать балансы жестом переворота Эмитент - Отправить отзыв Подписано Подробности Проверьте подключение с интернетом или переключитесь на другую сеть @@ -454,7 +455,6 @@ Сумма Вычесть из суммы отправки Сумма к получению %s - Поддержка Транзакция не выполнена Причина: %1$s\nКод: %2$s Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 6f802b6be1..bef56ab91b 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -130,7 +130,6 @@ 連結更多錢包 App Currency 發行人 - 發送反饋 簽署 更多 檢查您的網路連接或切換到其他網絡 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 246e7b197e..50a26c94a7 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -129,6 +129,7 @@ Start Submit Success + Support Swap terms and conditions Transaction failed @@ -180,11 +181,11 @@ Privacy policy %s hashes Card ID + Contact to Support Link More Cards App Currency Flip-to-Hide Balances Issuer - Send Feedback Signed Details Check your internet connection or switch to a different network @@ -456,7 +457,6 @@ Amount Subtract from send amount The recipient will receive %s - Support The transaction is not completed Reason: %1$s\nCode: %2$s Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? 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 ce176c7174..204b99d3fb 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 @@ -20,7 +20,7 @@ internal sealed class SendAlertState { ) : SendAlertState() { override val message: TextReference = resourceReference(R.string.common_unknown_error) override val confirmButtonText: TextReference = - resourceReference(id = R.string.send_alert_button_request_support) + resourceReference(id = R.string.common_support) } data class TransactionError( @@ -35,7 +35,7 @@ internal sealed class SendAlertState { formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code), ) override val confirmButtonText: TextReference = - resourceReference(id = R.string.send_alert_button_request_support) + resourceReference(id = R.string.common_support) } data class DemoMode( From f3906a385aadc82744fdf0f234fdf7b491df8ef8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Feb 2024 16:01:42 +0500 Subject: [PATCH 25/33] Updated on 2026-08-14 --- .../features/send/impl/presentation/SendFragment.kt | 2 +- .../impl/presentation/state/SendNotificationFactory.kt | 4 +++- .../features/send/impl/presentation/state/SendUiState.kt | 5 ----- .../features/send/impl/presentation/state/StateRouter.kt | 3 ++- .../presentation/state/fee/FeeNotificationFactory.kt | 5 ++++- .../send/impl/presentation/ui/SendNavigationButtons.kt | 2 +- .../features/send/impl/presentation/ui/SendScreen.kt | 9 ++++----- .../send/impl/presentation/viewmodel/SendClickIntents.kt | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) 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 1189a49843..539fd6d734 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 @@ -73,7 +73,7 @@ internal class SendFragment : ComposeFragment() { SystemBarsEffect { setSystemBarsColor(systemBarsColor) } - SendScreen(viewModel.uiState) + SendScreen(viewModel.uiState, viewModel.stateRouter.currentState) } override fun onDestroy() { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt index 6f3021a2b5..b253266100 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt @@ -17,16 +17,18 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import java.math.BigDecimal +@Suppress("LongParameterList") internal class SendNotificationFactory( private val cryptoCurrencyStatusProvider: Provider, private val coinCryptoCurrencyStatusProvider: Provider, private val currentStateProvider: Provider, private val userWalletProvider: Provider, private val currencyChecksRepository: CurrencyChecksRepository, + private val stateRouterProvider: Provider, private val clickIntents: SendClickIntents, ) { - fun create(): Flow> = currentStateProvider().currentState + fun create(): Flow> = stateRouterProvider().currentState .filter { it.type == SendUiStateType.Send } .map { val state = currentStateProvider() 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 8611ded493..bfc5fc8837 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 @@ -2,7 +2,6 @@ package com.tangem.features.send.impl.presentation.state import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import androidx.paging.PagingData import com.tangem.blockchain.common.transaction.Fee import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.event.StateEvent @@ -17,8 +16,6 @@ import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import java.math.BigDecimal /** @@ -32,8 +29,6 @@ internal data class SendUiState( val recipientState: SendStates.RecipientState? = null, val feeState: SendStates.FeeState? = null, val sendState: SendStates.SendState = SendStates.SendState(), - val recipientList: MutableStateFlow> = MutableStateFlow(PagingData.empty()), - val currentState: StateFlow, val isBalanceHidden: Boolean, val event: StateEvent, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index a9c6451ab9..fea5777945 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -28,7 +28,8 @@ internal class StateRouter( }, ) - val currentState: StateFlow = mutableCurrentState + val currentState: StateFlow + get() = mutableCurrentState fun popBackStack() { fragmentManager.get()?.popBackStack() 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 47b85049a3..f8c304748b 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 @@ -11,6 +11,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.R 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.viewmodel.SendClickIntents import com.tangem.utils.Provider @@ -21,16 +22,18 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import java.math.BigDecimal +@Suppress("LongParameterList") internal class FeeNotificationFactory( private val coinCryptoCurrencyStatusProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val currentStateProvider: Provider, private val userWalletProvider: Provider, + private val stateRouterProvider: Provider, private val clickIntents: SendClickIntents, private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, ) { - fun create() = currentStateProvider().currentState + fun create() = stateRouterProvider().currentState .filter { it.type == SendUiStateType.Fee } .map { val state = currentStateProvider() 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 6fe98a42ac..2af0c26442 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 @@ -121,7 +121,7 @@ private fun SendPrimaryNavigationButton( PrimaryButtonsDone( textRes = textId, txUrl = txUrl, - onExploreClick = { uiState.clickIntents.onExploreClick(txUrl) }, + onExploreClick = uiState.clickIntents::onExploreClick, onShareClick = uiState.clickIntents::onShareClick, onDoneClick = buttonClick, modifier = Modifier, 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 6e862d9e07..d0a3818435 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 @@ -15,7 +15,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.paging.compose.collectAsLazyPagingItems import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R @@ -26,11 +25,11 @@ 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) { - val currentState = uiState.currentState.collectAsStateWithLifecycle() - val isSuccess = uiState.sendState.isSuccess +internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow) { + val currentState = currentStateFlow.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } BackHandler { uiState.clickIntents.onBackClick() } Column( @@ -45,7 +44,7 @@ internal fun SendScreen(uiState: SendUiState) { SendUiStateType.Amount -> R.string.send_amount_label SendUiStateType.Recipient -> R.string.send_recipient_label SendUiStateType.Fee -> R.string.common_fee_selector_title - SendUiStateType.Send -> if (!isSuccess) R.string.send_confirm_label else null + SendUiStateType.Send -> if (!uiState.sendState.isSuccess) R.string.send_confirm_label else null else -> null } val iconRes = if (currentState.value.type == SendUiStateType.Recipient) { 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 6c66189f98..f2eedb220d 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 @@ -57,7 +57,7 @@ internal interface SendClickIntents { fun showFee() - fun onExploreClick(txUrl: String) + fun onExploreClick() fun onShareClick() From 0fae3c188c75ceb7db6038a18eefe83b50d68b7f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Feb 2024 16:11:50 +0500 Subject: [PATCH 26/33] Updated on 2026-08-14 --- .../tap/di/domain/TxHistoryDomainModule.kt | 7 ++++ .../repository/DefaultTxHistoryRepository.kt | 40 +++++++++++++++++++ .../repository/TxHistoryRepository.kt | 8 ++++ .../usecase/GetFixedTxHistoryItemsUseCase.kt | 36 +++++++++++++++++ .../usecase/GetTxHistoryItemsUseCase.kt | 2 +- 5 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt index 7ba9e05cff..7f0c9abe88 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.tokens.* import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase +import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import dagger.Module @@ -34,4 +35,10 @@ internal object TxHistoryDomainModule { ): GetExplorerTransactionUrlUseCase { return GetExplorerTransactionUrlUseCase(repository = txHistoryRepository) } + + @Provides + @ViewModelScoped + fun providesGetFixedTxHistoryItemsUseCase(txHistoryRepository: TxHistoryRepository): GetFixedTxHistoryItemsUseCase { + return GetFixedTxHistoryItemsUseCase(repository = txHistoryRepository) + } } \ 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 9120a85ef1..7b6663b564 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 @@ -10,11 +10,13 @@ import com.tangem.datasource.local.txhistory.TxHistoryItemsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.txhistory.models.Page import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.walletmanager.WalletManagersFacade +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 @@ -25,6 +27,7 @@ class DefaultTxHistoryRepository( private val userWalletsStore: UserWalletsStore, private val txHistoryItemsStore: TxHistoryItemsStore, ) : TxHistoryRepository { + private val sdkPageConverter by lazy { SdkPageConverter() } override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, currency: CryptoCurrency): Int { val userWallet = getUserWallet(userWalletId) @@ -74,6 +77,43 @@ class DefaultTxHistoryRepository( } } + 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() + } + + private fun getTxHistoryPageKey(currency: CryptoCurrency, userWalletId: UserWalletId, page: Page): String { + return "tx_history_page_${currency}_${userWalletId}_$page" + } + + private suspend fun fetchFixedSizeTxHistoryItems( + userWalletId: UserWalletId, + currency: CryptoCurrency, + pageSize: Int, + ) { + val wrappedItems = walletManagersFacade.getTxHistoryItems( + userWalletId = userWalletId, + currency = currency, + page = sdkPageConverter.convertBack(Page.Initial), + pageSize = pageSize, + ) + + txHistoryItemsStore.store(TxHistoryItemsStore.Key(userWalletId, currency), wrappedItems) + } + private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find user wallet with provided ID: $userWalletId" 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 381d469f98..b010a54db4 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 @@ -23,4 +23,12 @@ interface TxHistoryRepository { ): Flow> fun getTxExploreUrl(txHash: String, networkId: Network.ID): String + + @Throws(TxHistoryListError::class) + suspend fun getFixedSizeTxHistoryItems( + userWalletId: UserWalletId, + currency: CryptoCurrency, + pageSize: Int, + refresh: Boolean, + ): List } \ 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 new file mode 100644 index 0000000000..aa93bec246 --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetFixedTxHistoryItemsUseCase.kt @@ -0,0 +1,36 @@ +package com.tangem.domain.txhistory.usecase + +import arrow.core.Either +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** + * Returns first page with size [DEFAULT_PAGE_SIZE] of tx history. + * Page size is preserved to reuse cached data in Token Details Screen. + * + * IMPORTANT!!! + * If page size bigger than [DEFAULT_PAGE_SIZE] is needed consider to implement another use case + * without use of cached data or increase [DEFAULT_PAGE_SIZE] + */ +class GetFixedTxHistoryItemsUseCase( + private val repository: TxHistoryRepository, +) { + + operator fun invoke( + userWalletId: UserWalletId, + currency: CryptoCurrency, + pageSize: Int = DEFAULT_PAGE_SIZE, + refresh: Boolean = false, + ): Either>> { + return Either.catch { + flow { + emit(repository.getFixedSizeTxHistoryItems(userWalletId, currency, pageSize, refresh)) + } + }.mapLeft { TxHistoryListError.DataError(it) } + } +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt index e5570e8077..cb6724fc60 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt @@ -11,7 +11,7 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch -private const val DEFAULT_PAGE_SIZE = 50 +const val DEFAULT_PAGE_SIZE = 50 // TODO: Add tests class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { From f53a79e29c55cb3e522a688c6942abd11551811b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Feb 2024 16:12:21 +0500 Subject: [PATCH 27/33] Updated on 2026-08-14 --- .../domain/SendRecipientListContent.kt | 27 +- .../presentation/state/SendStateFactory.kt | 11 +- .../impl/presentation/state/SendUiState.kt | 3 +- .../recipient/SendRecipientListConverter.kt | 114 ++++---- .../recipient/SendRecipientStateConverter.kt | 3 + .../send/impl/presentation/ui/SendScreen.kt | 2 - .../ui/recipient/ListItemWithIcon.kt | 110 +++----- .../ui/recipient/SendRecipientContent.kt | 244 ++++++++---------- .../presentation/viewmodel/SendViewModel.kt | 65 +++-- 9 files changed, 245 insertions(+), 334 deletions(-) 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 47761c5956..0837f1f27c 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 @@ -1,23 +1,14 @@ package com.tangem.features.send.impl.presentation.domain import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference -import kotlinx.collections.immutable.PersistentList -@Immutable -internal sealed class SendRecipientListContent { - data class Item( - val id: String, - val title: TextReference, - val subtitle: TextReference, - val timestamp: TextReference? = null, - val subtitleEndOffset: Int = 0, - @DrawableRes val subtitleIconRes: Int? = null, - ) : SendRecipientListContent() - - data class Wallets( - val list: PersistentList, - val isWalletsOnly: Boolean, - ) : SendRecipientListContent() -} \ No newline at end of file +data class SendRecipientListContent( + val id: String, + val title: TextReference, + val subtitle: TextReference, + val timestamp: TextReference? = null, + val subtitleEndOffset: Int = 0, + @DrawableRes val subtitleIconRes: Int? = null, + val isVisible: Boolean = true, +) \ 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 10b562c4c5..619ec36aab 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 @@ -1,6 +1,5 @@ package com.tangem.features.send.impl.presentation.state -import androidx.paging.PagingData import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter @@ -23,7 +22,6 @@ 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.coroutines.flow.MutableStateFlow import timber.log.Timber @Suppress("LongParameterList") @@ -78,7 +76,6 @@ internal class SendStateFactory( // region UI states fun getInitialState(): SendUiState = SendUiState( clickIntents = clickIntents, - currentState = MutableStateFlow(SendUiCurrentScreen(type = SendUiStateType.None, isFromConfirmation = false)), event = consumedEvent(), isEditingDisabled = false, isBalanceHidden = false, @@ -109,17 +106,11 @@ internal class SendStateFactory( //endregion //region recipient - fun onLoadedRecipientList( - wallets: List, - txHistory: PagingData, - txHistoryCount: Int, - ) { + fun onLoadedRecipientList(wallets: List, txHistory: List): SendUiState = recipientListStateConverter.convert( wallets = wallets, txHistory = txHistory, - txHistoryCount = txHistoryCount, ) - } fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false): SendUiState { val state = currentStateProvider() 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 bfc5fc8837..34e2ddd3fe 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 @@ -59,7 +59,8 @@ internal sealed class SendStates { override val isPrimaryButtonEnabled: Boolean, val addressTextField: SendTextField.RecipientAddress, val memoTextField: SendTextField.RecipientMemo?, - val recipients: MutableStateFlow> = MutableStateFlow(PagingData.empty()), + val recent: ImmutableList, + val wallets: ImmutableList, val network: String, val isValidating: Boolean = false, ) : SendStates() 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/SendRecipientListConverter.kt index 0734f7a243..046c7a153d 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/SendRecipientListConverter.kt @@ -1,6 +1,5 @@ package com.tangem.features.send.impl.presentation.state.recipient -import androidx.paging.* import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList @@ -17,77 +16,66 @@ import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.utils.Provider import com.tangem.utils.toFormattedCurrencyString import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.update internal class SendRecipientListConverter( private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, ) { - fun convert(wallets: List, txHistory: PagingData, txHistoryCount: Int) { - val filteredWallets = wallets.filterNotNull() - .groupBy { item -> item.name } - .values.flatten() - .mapIndexed { index, item -> - item.copy( - name = "${item.name} ${index.inc()}", - ) - } - - val walletsItem = getWalletItems(filteredWallets, txHistoryCount) - + fun convert(wallets: List, txHistory: List): SendUiState { val cryptoCurrency = cryptoCurrencyStatusProvider().currency - currentStateProvider().recipientList.update { - if (txHistoryCount == 0) { - PagingData.from(listOf(walletsItem)) - } else { - txHistory.filter { item -> - val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer - val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User - val isSingleAddress = if (item.isOutgoing) { - item.destinationType is TxHistoryItem.DestinationType.Single - } else { - item.sourceType is TxHistoryItem.SourceType.Single - } - isTransfer && isSingleAddress && isNotContract - }.map { tx -> - SendRecipientListContent.Item( - id = tx.txHash, - title = tx.extractAddress(), - subtitle = stringReference(tx.getAmount(cryptoCurrency).trim()), - timestamp = tx.extractTimestamp(), - subtitleEndOffset = cryptoCurrency.symbol.length, - subtitleIconRes = tx.extractIconRes(), - ) - }.insertWallets(walletsItem) - } - } - } + val state = currentStateProvider() + val recipientState = state.recipientState ?: return state - private fun PagingData.insertWallets( - wallets: SendRecipientListContent.Wallets, - ): PagingData { - return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> - return@insertSeparators when { - before == null && after is SendRecipientListContent.Item -> wallets - else -> null - } - } - } - - private fun getWalletItems(wallets: List, txHistoryCount: Int): SendRecipientListContent.Wallets { - return SendRecipientListContent.Wallets( - wallets.map { - SendRecipientListContent.Item( - id = it.address, - title = TextReference.Str(it.address), - subtitle = TextReference.Str(it.name), - ) - }.toPersistentList(), - isWalletsOnly = txHistoryCount == 0, + 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), + ) + } + } + .flatten() + .toPersistentList() + + private fun List.filterRecipients(cryptoCurrency: CryptoCurrency) = this.filter { item -> + val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer + val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User + val isSingleAddress = if (item.isOutgoing) { + item.destinationType is TxHistoryItem.DestinationType.Single + } else { + item.sourceType is TxHistoryItem.SourceType.Single + } + isTransfer && isSingleAddress && isNotContract + } + .take(RECENT_LIST_SIZE) + .map { tx -> + SendRecipientListContent( + id = tx.txHash, + title = tx.extractAddress(), + subtitle = stringReference(tx.getAmount(cryptoCurrency).trim()), + timestamp = tx.extractTimestamp(), + subtitleEndOffset = cryptoCurrency.symbol.length, + subtitleIconRes = tx.extractIconRes(), + ) + }.toPersistentList() + private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) { when (val destination = destinationType) { is TxHistoryItem.DestinationType.Multiple -> TextReference.Res( @@ -122,4 +110,8 @@ internal class SendRecipientListConverter( val time = timestampInMillis.toTimeFormat() return TextReference.Res(R.string.send_date_format, wrappedList(date, time)) } + + companion object { + private const val RECENT_LIST_SIZE = 10 + } } \ No newline at end of file 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 a558d4ded6..ae375c97e2 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 @@ -5,6 +5,7 @@ import com.tangem.features.send.impl.presentation.state.SendStates 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, @@ -25,6 +26,8 @@ internal class SendRecipientStateConverter( memoTextField = memoFieldConverter.convertOrNull(), network = cryptoCurrencyStatusProvider().currency.network.name, isPrimaryButtonEnabled = false, + wallets = persistentListOf(), + recent = persistentListOf(), ) } } \ No newline at end of file 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 d0a3818435..2e0a451ec3 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 @@ -82,7 +82,6 @@ private fun SendScreenContent( currentState: State, modifier: Modifier = Modifier, ) { - val recipientList = uiState.recipientList.collectAsLazyPagingItems() AnimatedContent( targetState = currentState.value, label = "Send Scree Navigation", @@ -97,7 +96,6 @@ private fun SendScreenContent( SendUiStateType.Recipient -> SendRecipientContent( uiState = uiState.recipientState, clickIntents = uiState.clickIntents, - recipientList = recipientList, ) SendUiStateType.Fee -> SendSpeedAndFeeContent( state = uiState.feeState, 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 d16846ae48..c33885f290 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 @@ -3,9 +3,7 @@ package com.tangem.features.send.impl.presentation.ui.recipient import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -18,9 +16,6 @@ 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 androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import androidx.constraintlayout.compose.Visibility 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 @@ -39,7 +34,6 @@ import com.tangem.features.send.impl.R * @param subtitleEndOffset offset for subtitle ellipsis * @param subtitleIconRes icon */ -@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod") @Composable fun ListItemWithIcon( title: String, @@ -51,80 +45,60 @@ fun ListItemWithIcon( @DrawableRes subtitleIconRes: Int? = null, ) { val hapticFeedback = rememberHapticFeedback(state = title, onAction = onClick) - ConstraintLayout( + Row( modifier = modifier .fillMaxWidth() .clickable { hapticFeedback() } .padding(horizontal = TangemTheme.dimens.spacing12), ) { - val (iconRef, titleRef, subtitleRef, subtitleIconRef) = createRefs() - - val spacing2 = TangemTheme.dimens.spacing2 - val spacing8 = TangemTheme.dimens.spacing8 - val spacing10 = TangemTheme.dimens.spacing10 - val spacing12 = TangemTheme.dimens.spacing12 IdentIcon( address = title, modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing8) .size(TangemTheme.dimens.size40) - .clip(RoundedCornerShape(TangemTheme.dimens.radius20)) - .constrainAs(iconRef) { - start.linkTo(parent.start) - top.linkTo(parent.top, margin = spacing8) - bottom.linkTo(parent.bottom, margin = spacing8) - }, + .clip(RoundedCornerShape(TangemTheme.dimens.radius20)), ) - EllipsisText( - text = title, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Justify, - ellipsis = TextEllipsis.Middle, + Column( modifier = Modifier - .constrainAs(titleRef) { - start.linkTo(iconRef.end, margin = spacing12) - end.linkTo(parent.end) - top.linkTo(parent.top, margin = spacing10) - width = Dimension.fillToConstraints - }, - ) - Icon( - painter = painterResource(id = subtitleIconRes ?: R.drawable.ic_arrow_down_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - modifier = Modifier - .size(TangemTheme.dimens.size16) - .background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f), CircleShape) - .constrainAs(subtitleIconRef) { - start.linkTo(iconRef.end, margin = spacing12) - top.linkTo(titleRef.bottom) - bottom.linkTo(parent.bottom, margin = spacing10) - visibility = if (subtitleIconRes == null) Visibility.Gone else Visibility.Visible - }, - ) - - val (text, offset) = remember(subtitle, info) { - if (info != null) { - val suffix = ", $info" - subtitle + suffix to suffix.length + subtitleEndOffset - } else { - subtitle to 0 + .padding(vertical = TangemTheme.dimens.spacing10) + .padding(start = TangemTheme.dimens.spacing12), + ) { + EllipsisText( + text = title, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Justify, + ellipsis = TextEllipsis.Middle, + modifier = Modifier, + ) + Row { + if (subtitleIconRes != null) { + Icon( + painter = painterResource(id = subtitleIconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .background(TangemTheme.colors.background.tertiary, CircleShape), + ) + } + val (text, offset) = remember(subtitle, info) { + if (info != null) { + val suffix = ", $info" + subtitle + suffix to suffix.length + subtitleEndOffset + } else { + subtitle to 0 + } + } + EllipsisText( + text = text, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ellipsis = TextEllipsis.OffsetEnd(offsetEnd = offset), + modifier = Modifier.padding(start = TangemTheme.dimens.spacing2), + ) } } - EllipsisText( - text = text, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ellipsis = TextEllipsis.OffsetEnd(offsetEnd = offset), - modifier = Modifier - .constrainAs(subtitleRef) { - start.linkTo(subtitleIconRef.end, margin = spacing2, goneMargin = spacing12) - end.linkTo(parent.end) - top.linkTo(titleRef.bottom) - bottom.linkTo(parent.bottom, margin = spacing10) - width = Dimension.fillToConstraints - }, - ) } } 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 c80581a8f8..4c490fb1bc 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 @@ -1,8 +1,9 @@ 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.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -17,9 +18,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.itemContentType -import androidx.paging.compose.itemKey import com.tangem.core.ui.components.inputrow.InputRowRecipient import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -29,18 +27,17 @@ import com.tangem.features.send.impl.presentation.domain.SendRecipientListConten import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import kotlinx.collections.immutable.ImmutableList private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY" -private const val MY_WALLETS_HEADER_KEY = "MY_WALLETS_HEADER_KEY" @Composable -internal fun SendRecipientContent( - uiState: SendStates.RecipientState?, - clickIntents: SendClickIntents, - recipientList: LazyPagingItems, -) { +internal fun SendRecipientContent(uiState: SendStates.RecipientState?, clickIntents: SendClickIntents) { if (uiState == null) return + val recipients = uiState.recent + val wallets = uiState.wallets + val memoField = uiState.memoTextField val address = uiState.addressTextField val isValidating by remember(uiState.isValidating) { derivedStateOf { uiState.isValidating } } val isError by remember(address.isError) { derivedStateOf { address.isError } } @@ -72,7 +69,7 @@ internal fun SendRecipientContent( ) } } - uiState.memoTextField?.let { memoField -> + if (memoField != null) { item(key = MEMO_FIELD_KEY) { val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText TextFieldWithPaste( @@ -89,144 +86,115 @@ internal fun SendRecipientContent( ) } } - recipientListItem( - recipientList = recipientList, - clickIntents = clickIntents, + listHeaderItem( + titleRes = R.string.send_recipient_wallets_title, + isVisible = wallets.isNotEmpty() && wallets.first().isVisible, + isFirst = true, ) + listItem(wallets, clickIntents, isLast = recipients.isEmpty()) + listHeaderItem( + titleRes = R.string.send_recent_transactions, + isVisible = recipients.isNotEmpty() && recipients.first().isVisible, + isFirst = wallets.isEmpty(), + ) + listItem(recipients, clickIntents, isLast = true) } } -@Suppress("LongMethod") @OptIn(ExperimentalFoundationApi::class) -private fun LazyListScope.recipientListItem( - recipientList: LazyPagingItems, +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(), + ) { + val (topPadding, paddingFromTop) = if (isFirst) { + TangemTheme.dimens.spacing20 to TangemTheme.dimens.spacing12 + } else { + TangemTheme.dimens.spacing0 to TangemTheme.dimens.spacing8 + } + val topRadius = if (isFirst) { + TangemTheme.dimens.radius12 + } else { + TangemTheme.dimens.radius0 + } + Text( + text = stringResource(titleRes), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .padding(top = topPadding) + .clip( + RoundedCornerShape( + topEnd = topRadius, + topStart = topRadius, + ), + ) + .background(TangemTheme.colors.background.action) + .padding( + top = paddingFromTop, + bottom = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.listItem( + list: ImmutableList, clickIntents: SendClickIntents, + isLast: Boolean, ) { items( - count = recipientList.itemCount, - key = recipientList.itemKey { - when (it) { - is SendRecipientListContent.Wallets -> MY_WALLETS_HEADER_KEY - is SendRecipientListContent.Item -> it.id - } - }, - contentType = recipientList.itemContentType { it::class.java }, + count = list.size, + key = { list[it].id }, + contentType = { list[it]::class.java }, ) { index -> - recipientList[index]?.let { item -> - when (item) { - is SendRecipientListContent.Wallets -> { - RecipientWalletListItem( - item = item, - clickIntents = clickIntents, - modifier = Modifier - .animateItemPlacement() - .padding(top = TangemTheme.dimens.spacing20) - .then( - if (index == 0) { - val bottomRadius = if (item.isWalletsOnly) { - TangemTheme.dimens.radius12 - } else { - TangemTheme.dimens.radius0 - } - Modifier.clip( - RoundedCornerShape( - topEnd = TangemTheme.dimens.radius12, - topStart = TangemTheme.dimens.radius12, - bottomStart = bottomRadius, - bottomEnd = bottomRadius, - ), - ) - } else { - Modifier - }, - ), - ) - } - is SendRecipientListContent.Item -> { - val title = item.title.resolveReference() - ListItemWithIcon( - title = item.title.resolveReference(), - subtitle = item.subtitle.resolveReference(), - info = item.timestamp?.resolveReference(), - subtitleEndOffset = item.subtitleEndOffset, - subtitleIconRes = item.subtitleIconRes, - modifier = Modifier - .then( - if (index == recipientList.itemCount - 1) { - Modifier - .padding(bottom = TangemTheme.dimens.spacing20) - .clip( - RoundedCornerShape( - bottomEnd = TangemTheme.dimens.radius12, - bottomStart = TangemTheme.dimens.radius12, - ), - ) - } else { - Modifier - }, - ) - .background(TangemTheme.colors.background.action), - onClick = { - clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress) + 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(), + ) { + ListItemWithIcon( + title = title, + subtitle = item.subtitle.resolveReference(), + info = item.timestamp?.resolveReference(), + subtitleEndOffset = item.subtitleEndOffset, + subtitleIconRes = item.subtitleIconRes, + onClick = { clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress) }, + modifier = Modifier + .then( + if (isLast && index == list.lastIndex) { + Modifier + .padding(bottom = TangemTheme.dimens.spacing12) + .clip( + shape = RoundedCornerShape( + bottomStart = TangemTheme.dimens.radius16, + bottomEnd = TangemTheme.dimens.radius16, + ), + ) + } else { + Modifier }, ) - } - } - } - } -} - -@Composable -private fun RecipientWalletListItem( - item: SendRecipientListContent.Wallets, - clickIntents: SendClickIntents, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .background(TangemTheme.colors.background.action) - .padding(top = TangemTheme.dimens.spacing12), - ) { - if (item.list.isNotEmpty()) { - Text( - text = stringResource(R.string.send_recipient_wallets_title), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .fillMaxWidth() - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing8, - ), - ) - } - item.list.forEachIndexed { _, wallet -> - val title = wallet.title.resolveReference() - ListItemWithIcon( - title = wallet.title.resolveReference(), - subtitle = wallet.subtitle.resolveReference(), - onClick = { clickIntents.onRecipientAddressValueChange(title, EnterAddressSource.RecentAddress) }, - ) - } - if (!item.isWalletsOnly) { - val topPadding = if (item.list.isNotEmpty()) { - TangemTheme.dimens.spacing8 - } else { - TangemTheme.dimens.spacing0 - } - Text( - text = stringResource(R.string.send_recent_transactions), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .fillMaxWidth() - .padding( - top = topPadding, - bottom = TangemTheme.dimens.spacing8, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ), + .background(TangemTheme.colors.background.action), ) } } 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 c9969641c5..7b47227a2e 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 @@ -4,8 +4,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* -import androidx.paging.PagingData -import androidx.paging.cachedIn import arrow.core.Either import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData @@ -28,8 +26,7 @@ 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.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +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 @@ -54,6 +51,7 @@ 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 @@ -72,8 +70,7 @@ internal class SendViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, @@ -106,7 +103,8 @@ internal class SendViewModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() private var innerRouter: InnerSendRouter by Delegates.notNull() - private var stateRouter: StateRouter by Delegates.notNull() + var stateRouter: StateRouter by Delegates.notNull() + private set private val stateFactory = SendStateFactory( clickIntents = this, @@ -142,6 +140,7 @@ internal class SendViewModel @Inject constructor( coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, + stateRouterProvider = Provider { stateRouter }, clickIntents = this, getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase, ) @@ -151,6 +150,7 @@ internal class SendViewModel @Inject constructor( coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, + stateRouterProvider = Provider { stateRouter }, currencyChecksRepository = currencyChecksRepository, clickIntents = this, ) @@ -190,7 +190,6 @@ internal class SendViewModel @Inject constructor( fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) { innerRouter = router this.stateRouter = stateRouter - uiState = uiState.copy(currentState = stateRouter.currentState) } private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) { @@ -301,12 +300,10 @@ internal class SendViewModel @Inject constructor( combine( flow = getUserWallets().conflate(), flow2 = getTxHistory().conflate(), - flow3 = getTxHistoryCount().conflate(), - ) { wallets, txHistory, txHistoryCount -> - stateFactory.onLoadedRecipientList( + ) { wallets, txHistory -> + uiState = stateFactory.onLoadedRecipientList( wallets = wallets, txHistory = txHistory, - txHistoryCount = txHistoryCount, ) } .flowOn(dispatchers.io) @@ -348,34 +345,18 @@ internal class SendViewModel @Inject constructor( } } - private fun getTxHistory(): Flow> { - return txHistoryItemsUseCase( + private fun getTxHistory(): Flow> { + return getFixedTxHistoryItemsUseCase( userWalletId = userWalletId, currency = cryptoCurrency, ).fold( - ifRight = { - it.distinctUntilChanged().cachedIn(viewModelScope) - }, - ifLeft = { - emptyFlow() - }, + ifRight = { it.distinctUntilChanged() }, + ifLeft = { emptyFlow() }, ) } - private fun getTxHistoryCount(): Flow { - return flow { - txHistoryItemsCountUseCase( - userWalletId = userWalletId, - currency = cryptoCurrency, - ).fold( - ifRight = { emit(it) }, - ifLeft = { emit(0) }, - ) - } - } - private fun onStateActive() { - uiState.currentState + stateRouter.currentState .onEach { when (it.type) { SendUiStateType.Fee -> if (!it.isFromConfirmation) loadFee() @@ -499,11 +480,13 @@ internal class SendViewModel @Inject constructor( } private suspend fun validateAddress(value: String): Boolean { - return validateWalletAddressUseCase( + val isValidAddress = validateWalletAddressUseCase( userWalletId = userWalletId, network = cryptoCurrency.network, address = value, ).getOrElse { false } + onEnteredValidAddress(isValidAddress) + return isValidAddress } private suspend fun checkIfXrpAddressValue(value: String): Boolean { @@ -515,6 +498,16 @@ internal class SendViewModel @Inject constructor( true } ?: 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(), + ), + ) + } // endregion // region fee @@ -613,9 +606,9 @@ internal class SendViewModel @Inject constructor( analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) } - override fun onExploreClick(txUrl: String) { + override fun onExploreClick() { analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked) - innerRouter.openUrl(txUrl) + innerRouter.openUrl(uiState.sendState.txUrl) } override fun onShareClick() { From 5ef2873135ee9b04cf1cd0bcb2e12d98680ea52d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Feb 2024 16:31:12 +0500 Subject: [PATCH 28/33] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 62e755b2c6..5cd4aa90a1 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -544,6 +544,7 @@ Комиссии В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя В сумму включена комиссия провайдера сервиса. + Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии Недостаточно средств для оплаты комиссии на вашем %1$s кошельке для создания транзакции. Сначала пополните свой %2$s кошелек. Транзакция в процессе… Подождите diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 50a26c94a7..a089602733 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -561,6 +561,7 @@ Fees The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. The amount includes the service provider\'s fee. + Sending amount will be reduced to cover the selected fee level Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first. Transaction in progress... Waiting diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 84aba17a86..b10f504d1e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1241,7 +1241,7 @@ internal class StateBuilder( private fun createNetworkFeeCoverageNotificationConfig(): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.send_network_fee_warning_title), - subtitle = resourceReference(R.string.send_network_fee_warning_content), + subtitle = resourceReference(R.string.swapping_network_fee_warning_content), iconResId = R.drawable.img_attention_20, ) } From b7c6ea61a68252c11c9b9797f5c65949fee3b260 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Feb 2024 11:20:48 +0500 Subject: [PATCH 29/33] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 10 +++--- core/res/src/main/res/values/strings.xml | 13 ++++--- .../impl/presentation/state/SendAlertState.kt | 8 +++++ .../state/SendEventStateFactory.kt | 24 +++++++++++++ .../presentation/state/SendNotification.kt | 27 ++++++++++---- .../state/SendNotificationFactory.kt | 35 ++++++++++++++++--- .../state/fee/FeeNotificationFactory.kt | 15 -------- .../state/fee/SendFeeNotification.kt | 5 --- .../ui/fee/SendSpeedAndFeeContent.kt | 5 +-- .../presentation/ui/fee/SendSpeedSelector.kt | 5 +-- .../viewmodel/SendClickIntents.kt | 7 ++-- .../presentation/viewmodel/SendViewModel.kt | 15 +++++--- 12 files changed, 112 insertions(+), 57 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 5cd4aa90a1..2629c88c99 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -480,14 +480,12 @@ Комиссия не превысит Максимальная cумма комиссии Сетевая комиссия - Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии + Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s. Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств Увеличение комиссии Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. - Оставить %s XTZ - Отправить все Установлена высокая комиссия Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению @@ -497,8 +495,10 @@ Сумма отправки не может быть менее %1$s Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции Возможны задержки по транзакции - Уменьшить до [%s] - Уменьшить на [%s] + Лимит транзакции + Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. + Уменьшить до %s + Уменьшить на %s Необязательное Последние Получатель diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index a089602733..a4fd2f577a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -459,8 +459,9 @@ The recipient will receive %s The transaction is not completed Reason: %1$s\nCode: %2$s - Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? - Вычесть + Not enough funds to cover the network commission. Subtract the commission from the amount sent? + Subtract + Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? Confirm %1$s at %2$s Address @@ -494,8 +495,6 @@ Total exceeds balance Fee is increased The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. - Reduce by %s XTZ - No, send all Custom fee is high The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. The included commission exceeds the transfer amount, leading to a negative value @@ -506,11 +505,11 @@ Kindly be aware that your transaction may experience delays under specific fee settings Transaction delays are possible Transaction limitation - Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. + Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Existential deposit The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s. - Reduce by [%s] - Reduce to [%s] + Reduce by %s + Reduce to %s Optional Please align your QR code with the square to scan it. Ensure you scan %s network address. Recent 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 204b99d3fb..30becd1f7e 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 @@ -50,6 +50,14 @@ internal sealed class SendAlertState { override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title) } + data class FeeTooLow( + override val onConfirmClick: () -> Unit, + ) : SendAlertState() { + override val title: TextReference? = null + override val message: TextReference = resourceReference(id = R.string.send_alert_fee_too_low_text) + override val confirmButtonText: TextReference = resourceReference(R.string.common_continue) + } + data class FeeCoverage( override val onConfirmClick: (() -> Unit), ) : SendAlertState() { 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 1bfe20a62b..fbbc8953ab 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,6 +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.parseToBigDecimal 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 @@ -88,6 +89,29 @@ internal class SendEventStateFactory( } } + fun getFeeTooLowAlert(onConsume: () -> Unit): SendUiState { + val state = currentStateProvider() + val feeSelectorState = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state + val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return state + val minimumValue = multipleFees.minimum.amount.value ?: return state + val customAmount = feeSelectorState.customValues.firstOrNull() ?: return state + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) + + val isFeeTooLow = feeSelectorState.selectedFee == FeeType.Custom && minimumValue > customValue + if (!isFeeTooLow) return state + + return state.copy( + event = triggeredEvent( + data = SendEvent.ShowAlert( + SendAlertState.FeeTooLow( + onConfirmClick = clickIntents::showSend, + ), + ), + onConsume = onConsume, + ), + ) + } + fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState { val state = currentStateProvider() return state.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index 4ab6952836..bf5148d51e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -12,12 +12,14 @@ internal sealed class SendNotification(val config: NotificationConfig) { title: TextReference, subtitle: TextReference, buttonState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, ) : SendNotification( config = NotificationConfig( title = title, subtitle = subtitle, iconResId = R.drawable.ic_alert_24, buttonsState = buttonState, + onCloseClick = onCloseClick, ), ) { @@ -45,12 +47,17 @@ internal sealed class SendNotification(val config: NotificationConfig) { val cryptoCurrency: String, val utxoLimit: String, val amountLimit: String, + val onConfirmClick: () -> Unit, ) : Error( title = resourceReference(R.string.send_notifiaction_transaction_limit_title), subtitle = resourceReference( - R.string.send_notifiaction_transaction_limit_text, + R.string.send_notification_transaction_limit_text, wrappedList(cryptoCurrency, utxoLimit, amountLimit), ), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_reduce_to, wrappedList(amountLimit)), + onClick = onConfirmClick, + ), ) } @@ -58,27 +65,28 @@ internal sealed class SendNotification(val config: NotificationConfig) { title: TextReference, subtitle: TextReference, buttonsState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, ) : SendNotification( config = NotificationConfig( title = title, subtitle = subtitle, iconResId = R.drawable.img_attention_20, buttonsState = buttonsState, + onCloseClick = onCloseClick, ), ) { data class HighFeeError( val amount: String, val onConfirmClick: () -> Unit, - val onDismissClick: () -> Unit, + val onCloseClick: () -> Unit, ) : Warning( title = resourceReference(R.string.send_notification_high_fee_title), subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)), - buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig( - primaryText = resourceReference(R.string.send_notification_fee_too_high_accept, wrappedList(amount)), - onPrimaryClick = onConfirmClick, - secondaryText = resourceReference(R.string.send_notification_fee_too_high_ignore), - onSecondaryClick = onDismissClick, + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)), + onClick = onConfirmClick, ), + onCloseClick = onCloseClick, ) data class ExistentialDeposit(val deposit: String) : Warning( @@ -96,5 +104,10 @@ internal sealed class SendNotification(val config: NotificationConfig) { formatArgs = wrappedList(amountReducedBy, amountReduced), ), ) + + data object FeeTooLow : Warning( + title = resourceReference(id = R.string.send_notification_transaction_delay_title), + subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text), + ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt index b253266100..12ea309a87 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt @@ -1,11 +1,15 @@ package com.tangem.features.send.impl.presentation.state import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.wallets.models.UserWallet +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.utils.Provider import com.tangem.utils.isNullOrZero @@ -48,13 +52,16 @@ internal class SendNotificationFactory( addFeeCoverageNotification(sendState.isSubtract, sendAmount) addExistentialWarningNotification(feeAmount, sendAmount) addHighFeeWarningNotification(sendAmount, sendState.ignoreAmountReduce) + addTooLowNotification(feeState) }.toImmutableList() } - fun dismissHighFeeWarningState(): SendUiState { + fun dismissNotificationState(clazz: Class): SendUiState { val state = currentStateProvider() val sendState = state.sendState - val updatedNotifications = sendState.notifications.filterNot { it is SendNotification.Warning.HighFeeError } + val notificationsToRemove = sendState.notifications.filterIsInstance(clazz) + val updatedNotifications = sendState.notifications.toMutableList() + updatedNotifications.removeAll(notificationsToRemove) return state.copy( sendState = sendState.copy( ignoreAmountReduce = true, @@ -162,6 +169,13 @@ internal class SendNotificationFactory( cryptoAmount = utxoLimit.maxAmount, cryptoCurrency = cryptoCurrency, ), + onConfirmClick = { + val reduceTo = utxoLimit.maxAmount.toPlainString() + clickIntents.onAmountReduceClick( + reduceTo, + SendNotification.Error.TransactionLimitError::class.java, + ) + }, ), ) } @@ -207,9 +221,11 @@ internal class SendNotificationFactory( amount = TEZOS_FEE_THRESHOLD.toPlainString(), onConfirmClick = { val reduceTo = sendAmount.minus(TEZOS_FEE_THRESHOLD).toPlainString() - clickIntents.onAmountReduceClick(reduceTo) + clickIntents.onAmountReduceClick(reduceTo, SendNotification.Warning.HighFeeError::class.java) + }, + onCloseClick = { + clickIntents.onNotificationCancel(SendNotification.Warning.HighFeeError::class.java) }, - onDismissClick = clickIntents::onAmountReduceIgnoreClick, ), ) } @@ -261,6 +277,17 @@ internal class SendNotificationFactory( } } + private fun MutableList.addTooLowNotification(feeState: SendStates.FeeState) { + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return + val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return + val minimumValue = multipleFees.minimum.amount.value ?: return + val customAmount = feeSelectorState.customValues.firstOrNull() ?: return + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) + if (feeSelectorState.selectedFee == FeeType.Custom && minimumValue > customValue) { + add(SendNotification.Warning.FeeTooLow) + } + } + companion object { private const val CARDANO_MINIMUM = "1" private const val DOGECOIN_MINIMUM = "0.01" 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 f8c304748b..da1277412a 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 @@ -47,7 +47,6 @@ internal class FeeNotificationFactory( is FeeSelectorState.Content -> { val customFee = feeSelectorState.customValues val selectedFee = feeSelectorState.selectedFee - addTooLowNotification(feeSelectorState.fees, selectedFee, customFee) addTooHighNotification(feeSelectorState.fees, selectedFee, customFee) addExceedsBalanceNotification(feeState.fee) } @@ -61,20 +60,6 @@ internal class FeeNotificationFactory( } } - private fun MutableList.addTooLowNotification( - transactionFee: TransactionFee, - selectedFee: FeeType, - customFee: List, - ) { - val multipleFees = transactionFee as? TransactionFee.Choosable ?: return - val minimumValue = multipleFees.minimum.amount.value ?: return - val customAmount = customFee.firstOrNull() ?: return - val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) - if (selectedFee == FeeType.Custom && minimumValue > customValue) { - add(SendFeeNotification.Warning.TooLow) - } - } - private fun MutableList.addTooHighNotification( transactionFee: TransactionFee, selectedFee: FeeType, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt index 56add5cfe5..c82f072f6a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt @@ -20,11 +20,6 @@ sealed class SendFeeNotification(val config: NotificationConfig) { buttonsState = buttonsState, ), ) { - object TooLow : Warning( - title = resourceReference(id = R.string.send_notification_transaction_delay_title), - subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text), - ) - data class TooHigh( val value: String, ) : Warning( 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 5acd6b57b1..1695763049 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 @@ -74,10 +74,7 @@ private fun LazyListScope.middleNotifications( modifier: Modifier = Modifier, ) { notifications( - configs = configs.filter { - it is SendFeeNotification.Warning.TooLow || - it is SendFeeNotification.Warning.TooHigh - }.toImmutableList(), + configs = configs.filterIsInstance().toImmutableList(), modifier = modifier, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index 4d13b7d7a5..f5b10f50a8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -117,10 +117,7 @@ internal fun SendSpeedSelector( visible = fees.normal is Fee.Ethereum, label = "Custom fee appearance animation", ) { - val showWarning = state.notifications.any { - it is SendFeeNotification.Warning.TooHigh || - it is SendFeeNotification.Warning.TooLow - } + val showWarning = state.notifications.any { it is SendFeeNotification.Warning.TooHigh } SendSpeedSelectorItem( titleRes = R.string.common_fee_selector_option_custom, iconRes = R.drawable.ic_edit_24, 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 f2eedb220d..9af06fb346 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 @@ -3,6 +3,7 @@ package com.tangem.features.send.impl.presentation.viewmodel import com.tangem.domain.tokens.model.CryptoCurrency 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 @Suppress("TooManyFunctions") @@ -57,12 +58,14 @@ internal interface SendClickIntents { fun showFee() + fun showSend() + fun onExploreClick() fun onShareClick() - fun onAmountReduceClick(reducedAmount: String) + fun onAmountReduceClick(reducedAmount: String, clazz: Class) - fun onAmountReduceIgnoreClick() + fun onNotificationCancel(clazz: Class) // endregion } \ No newline at end of file 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 7b47227a2e..8267ee3c2a 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 @@ -394,6 +394,9 @@ internal class SendViewModel @Inject constructor( val currentState = stateRouter.currentState.value val isCurrentFee = currentState.type == SendUiStateType.Fee if (isCurrentFee) { + uiState = eventStateFactory.getFeeTooLowAlert( + onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + ) val isFeeCoverage = checkFeeCoverage(uiState, cryptoCurrencyStatus) if (isAmountSubtractAvailable && isFeeCoverage) { uiState = eventStateFactory.getFeeCoverageAlert( @@ -606,6 +609,10 @@ internal class SendViewModel @Inject constructor( analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) } + override fun showSend() { + stateRouter.showSend() + } + override fun onExploreClick() { analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked) innerRouter.openUrl(uiState.sendState.txUrl) @@ -615,14 +622,14 @@ internal class SendViewModel @Inject constructor( analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked) } - override fun onAmountReduceClick(reducedAmount: String) { + override fun onAmountReduceClick(reducedAmount: String, clazz: Class) { uiState = amountStateFactory.getOnAmountValueChange(reducedAmount) - uiState = sendNotificationFactory.dismissHighFeeWarningState() + uiState = sendNotificationFactory.dismissNotificationState(clazz) loadFee() } - override fun onAmountReduceIgnoreClick() { - uiState = sendNotificationFactory.dismissHighFeeWarningState() + override fun onNotificationCancel(clazz: Class) { + uiState = sendNotificationFactory.dismissNotificationState(clazz) } private fun verifyAndSendTransaction() { From e637ebdba0ee56438a2af98c014b6269a6e0fa87 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 Feb 2024 18:51:25 +0000 Subject: [PATCH 30/33] Updated on 2026-08-14 --- .../utils/ResponseCryptoCurrenciesFactory.kt | 33 ++++++++++++++----- .../model/warnings/CryptoCurrencyWarning.kt | 2 +- .../state/fee/FeeNotificationFactory.kt | 10 ++++++ .../state/fee/SendFeeNotification.kt | 12 ++++++- .../components/TokenDetailsNotification.kt | 9 ++++- .../TokenDetailsNotificationConverter.kt | 12 ++++++- gradle/dependencies.toml | 2 +- 7 files changed, 67 insertions(+), 13 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt index 5157d1430d..5435cebfd9 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt @@ -74,23 +74,40 @@ class ResponseCryptoCurrenciesFactory { ): CryptoCurrency.Coin? { val network = getNetwork(blockchain, responseToken.derivationPath, derivationStyleProvider) ?: return null - // workaround: Dischain was renamed but backend still returns the old name, - // get name and symbol from enum Blockchain until backend renamed - // [REDACTED_JIRA] - val name = if (blockchain == Blockchain.Dischain) blockchain.fullName else responseToken.name - val symbol = if (blockchain == Blockchain.Dischain) blockchain.currency else responseToken.symbol - return CryptoCurrency.Coin( id = getCoinId(network, blockchain.toCoinId()), network = network, - name = name, - symbol = symbol, + name = blockchain.getNameForCoin(responseToken), + symbol = blockchain.getSymbolForCoin(responseToken), decimals = responseToken.decimals, iconUrl = getCoinIconUrl(blockchain), isCustom = isCustomCoin(network), ) } + private fun Blockchain.getNameForCoin(responseToken: UserTokensResponse.Token): String { + return when (this) { + // workaround: Dischain was renamed but backend still returns the old name, + // get name and symbol from enum Blockchain until backend renamed + // [REDACTED_JIRA] + Blockchain.Dischain, + Blockchain.Arbitrum, + -> this.fullName + else -> responseToken.name + } + } + + private fun Blockchain.getSymbolForCoin(responseToken: UserTokensResponse.Token): String { + return when (this) { + // workaround: Dischain was renamed but backend still returns the old name, + // get name and symbol from enum Blockchain until backend renamed + // [REDACTED_JIRA] + Blockchain.Dischain, + -> this.currency + else -> responseToken.symbol + } + } + private fun createToken( blockchain: Blockchain, sdkToken: Token, diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index b90d787043..6e477d8a10 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -24,7 +24,7 @@ sealed class CryptoCurrencyWarning { val feeCurrencySymbol: String, ) : CryptoCurrencyWarning() - object SomeNetworksUnreachable : CryptoCurrencyWarning() + data object SomeNetworksUnreachable : CryptoCurrencyWarning() data class SomeNetworksNoAccount( val amountToCreateAccount: BigDecimal, 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 da1277412a..7b31b34d3d 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,9 +1,11 @@ package com.tangem.features.send.impl.presentation.state.fee +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -90,6 +92,7 @@ internal class FeeNotificationFactory( ifRight = { it }, ) ?: return + val mergeFeeNetworkName = cryptoCurrencyStatus.shouldMergeFeeNetworkName() when (warning) { is CryptoCurrencyWarning.BalanceNotEnoughForFee -> { add( @@ -99,6 +102,7 @@ internal class FeeNotificationFactory( currencyName = cryptoCurrencyStatus.currency.name, feeName = warning.coinCurrency.name, feeSymbol = warning.coinCurrency.symbol, + mergeFeeNetworkName = mergeFeeNetworkName, onClick = { clickIntents.onTokenDetailsClick( userWalletId = userWalletId, @@ -117,6 +121,7 @@ internal class FeeNotificationFactory( feeName = warning.feeCurrencyName, feeSymbol = warning.feeCurrencySymbol, networkName = warning.networkName, + mergeFeeNetworkName = mergeFeeNetworkName, onClick = currency?.let { { clickIntents.onTokenDetailsClick( @@ -132,6 +137,11 @@ internal class FeeNotificationFactory( } } + // workaround for networks that users have misunderstanding + private fun CryptoCurrencyStatus.shouldMergeFeeNetworkName(): Boolean { + return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum + } + companion object { private val FEE_MAX_DIFF = BigDecimal(5) private const val HIGH_FEE_DIFF_DECIMALS = 0 diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt index c82f072f6a..b3a2846e5f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeNotification.kt @@ -56,6 +56,7 @@ sealed class SendFeeNotification(val config: NotificationConfig) { val feeName: String, val feeSymbol: String, val networkName: String, + val mergeFeeNetworkName: Boolean = false, val onClick: (() -> Unit)? = null, ) : Error( title = resourceReference( @@ -69,7 +70,16 @@ sealed class SendFeeNotification(val config: NotificationConfig) { iconResId = networkIconId, buttonsState = onClick?.let { NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.common_buy_currency, wrappedList(feeName)), + text = resourceReference( + R.string.common_buy_currency, + wrappedList( + if (mergeFeeNetworkName) { + "$currencyName ($feeSymbol)" + } else { + feeName + }, + ), + ), onClick = onClick, ) }, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 185abeff5d..669c2e24f6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -99,6 +99,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { private val networkName: String, private val feeCurrencyName: String, private val feeCurrencySymbol: String, + val mergeFeeNetworkName: Boolean = false, private val onBuyClick: () -> Unit, ) : Warning( title = TextReference.Res( @@ -120,7 +121,13 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference( id = R.string.common_buy_currency, - formatArgs = wrappedList(feeCurrencySymbol), + formatArgs = wrappedList( + if (mergeFeeNetworkName) { + "$feeCurrencyName ($feeCurrencySymbol)" + } else { + feeCurrencySymbol + }, + ), ), onClick = onBuyClick, ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index bf37784e25..eaeb7f760a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -1,5 +1,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification @@ -28,9 +31,10 @@ internal class TokenDetailsNotificationConverter( return when (warning) { is CryptoCurrencyWarning.BalanceNotEnoughForFee -> NetworkFeeWithBuyButton( currency = warning.tokenCurrency, - networkName = warning.coinCurrency.name, + networkName = warning.coinCurrency.network.name, feeCurrencyName = warning.coinCurrency.name, feeCurrencySymbol = warning.coinCurrency.symbol, + mergeFeeNetworkName = warning.coinCurrency.shouldMergeFeeNetworkName(), onBuyClick = { clickIntents.onBuyCoinClick(warning.coinCurrency) }, ) is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { @@ -41,6 +45,7 @@ internal class TokenDetailsNotificationConverter( networkName = feeCurrency.network.name, feeCurrencyName = warning.feeCurrencyName, feeCurrencySymbol = warning.feeCurrencySymbol, + mergeFeeNetworkName = warning.currency.shouldMergeFeeNetworkName(), onBuyClick = { clickIntents.onBuyCoinClick(feeCurrency) }, ) } else { @@ -75,4 +80,9 @@ internal class TokenDetailsNotificationConverter( ) } } + + // workaround for networks that users have misunderstanding + private fun CryptoCurrency.shouldMergeFeeNetworkName(): Boolean { + return Blockchain.fromNetworkId(this.network.backendId) == Blockchain.Arbitrum + } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 47d3487f7f..5b36453cde 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -85,7 +85,7 @@ web3j = "4.10.1" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-497" +tangemBlockchainSdk = "develop-503" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-324" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 54cb670f229bb49d5c2b896f48e17084ef10eb3a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Feb 2024 15:38:53 +0000 Subject: [PATCH 31/33] Updated on 2026-08-14 --- .../java/com/tangem/tap/proxy/TransactionManagerImpl.kt | 6 +++++- .../txhistory/repository/DefaultTxHistoryRepository.kt | 9 ++++----- .../com/tangem/data/visa/utils/VisaTxDetailsFactory.kt | 8 +++++++- 3 files changed, 16 insertions(+), 7 deletions(-) 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 ec450b5f39..7794fc9ebc 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -17,6 +17,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.externallinkprovider.TxExploreState import com.tangem.blockchain.network.ResultChecker import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes @@ -117,7 +118,10 @@ class TransactionManagerImpl( override fun getExplorerTransactionLink(networkId: String, txAddress: String): String { val blockchain = Blockchain.fromNetworkId(networkId) ?: error("blockchain not found") - return blockchain.getExploreTxUrl(txAddress) + return when (val txUrlState = blockchain.getExploreTxUrl(txAddress)) { + TxExploreState.Unsupported -> "" + is TxExploreState.Url -> txUrlState.url + } } override fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { 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 7b6663b564..9f4801ff8c 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 @@ -4,6 +4,7 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.externallinkprovider.TxExploreState import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource import com.tangem.datasource.local.txhistory.TxHistoryItemsStore @@ -69,11 +70,9 @@ class DefaultTxHistoryRepository( override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String { val blockchain = Blockchain.fromId(networkId.value) - // TODO: Fix ton tx urls [REDACTED_TASK_KEY] - return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) { - "" - } else { - blockchain.getExploreTxUrl(txHash) + return when (val txExploreState = blockchain.getExploreTxUrl(txHash)) { + is TxExploreState.Url -> txExploreState.url + is TxExploreState.Unsupported -> "" } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt index ad0bbf4fb3..5108e0387e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt @@ -1,6 +1,7 @@ package com.tangem.data.visa.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.externallinkprovider.TxExploreState import com.tangem.domain.visa.model.VisaTxDetails import com.tangem.lib.visa.model.VisaTxHistoryResponse @@ -43,7 +44,12 @@ internal class VisaTxDetailsFactory { txHash = request.txHash, txStatus = request.txStatus, fiatCurrency = findCurrencyByNumericCode(request.transactionCurrencyCode), - exploreUrl = request.txHash?.let(walletBlockchain::getExploreTxUrl), + exploreUrl = request.txHash?.let { + when (val txUrl = walletBlockchain.getExploreTxUrl(it)) { + is TxExploreState.Url -> txUrl.url + is TxExploreState.Unsupported -> "" + } + }, ) } } \ No newline at end of file From 1bcc834e7e20c3a5ff139031a60fbff6e0d78506 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Feb 2024 00:08:47 +0500 Subject: [PATCH 32/33] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 2 - core/res/src/main/res/values/strings.xml | 2 - .../com/tangem/utils/coroutines/JobHolder.kt | 5 ++ .../impl/presentation/state/StateRouter.kt | 30 +++++---- .../fields/SendAmountFieldChangeConverter.kt | 7 ++- .../presentation/ui/fee/SendSpeedSubtract.kt | 61 ------------------- .../presentation/viewmodel/SendViewModel.kt | 31 ++++++---- 7 files changed, 46 insertions(+), 92 deletions(-) delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 2629c88c99..ea038372cd 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -453,8 +453,6 @@ Приготовьте свою карту Уже содержится в введенном адресе Сумма - Вычесть из суммы отправки - Сумма к получению %s Транзакция не выполнена Причина: %1$s\nКод: %2$s Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index a4fd2f577a..cada81a5ff 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -455,8 +455,6 @@ Get your card ready! Already included in the entered address Amount - Subtract from send amount - The recipient will receive %s The transaction is not completed Reason: %1$s\nCode: %2$s Not enough funds to cover the network commission. Subtract the commission from the amount sent? diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt index 3c0ca6bd8d..42c4c7b296 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt @@ -16,6 +16,11 @@ class JobHolder { this.job?.cancel() this.job = job } + + /** Cancel current [job] */ + fun cancel() { + job?.cancel() + } } fun Job.saveIn(jobHolder: JobHolder) = jobHolder.update(job = this) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index fea5777945..bf7b83183b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -14,23 +14,15 @@ internal class StateRouter( private val analyticsEventsHandler: AnalyticsEventHandler, private val isEditingDisabled: Boolean, ) { - private var mutableCurrentState: MutableStateFlow = MutableStateFlow( - if (isEditingDisabled) { - SendUiCurrentScreen( - type = SendUiStateType.None, - isFromConfirmation = false, - ) - } else { - SendUiCurrentScreen( - type = SendUiStateType.Recipient, - isFromConfirmation = false, - ) - }, - ) + private var mutableCurrentState: MutableStateFlow = MutableStateFlow(getInitState()) val currentState: StateFlow get() = mutableCurrentState + fun clear() { + mutableCurrentState.update { getInitState() } + } + fun popBackStack() { fragmentManager.get()?.popBackStack() } @@ -128,4 +120,16 @@ internal class StateRouter( private fun continueToSend(show: () -> Unit) { if (currentState.value.isFromConfirmation) showSend() else show() } + + private fun getInitState() = if (isEditingDisabled) { + SendUiCurrentScreen( + type = SendUiStateType.None, + isFromConfirmation = false, + ) + } else { + SendUiCurrentScreen( + type = SendUiStateType.Recipient, + isFromConfirmation = false, + ) + } } \ No newline at end of file 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 1b2a4d56f6..cd6254be4f 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 @@ -34,9 +34,10 @@ internal class SendAmountFieldChangeConverter( val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue val isExceedBalance = checkValue.checkExceedBalance(amountTextField) + val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isZero() else decimalCryptoValue.isZero() return state.copy( amountState = amountState.copy( - isPrimaryButtonEnabled = !isExceedBalance, + isPrimaryButtonEnabled = !isExceedBalance && !isZero, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, @@ -93,9 +94,9 @@ internal class SendAmountFieldChangeConverter( val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals) val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals) return if (amountTextField.isFiatValue) { - fiatDecimal > currencyFiatAmount || fiatDecimal.isZero() + fiatDecimal > currencyFiatAmount } else { - cryptoDecimal > currencyCryptoAmount || cryptoDecimal.isZero() + cryptoDecimal > currencyCryptoAmount } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt deleted file mode 100644 index 5fa7355bce..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSubtract.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.features.send.impl.presentation.ui.fee - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.TangemSwitch -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.ui.common.FooterContainer - -@Composable -internal fun SendSpeedSubtract( - receivingAmount: String, - isSubtract: Boolean, - onSelectClick: (Boolean) -> Unit, - modifier: Modifier = Modifier, -) { - val footerText = if (isSubtract) { - stringResource(R.string.send_amount_substract_footer, receivingAmount) - } else { - null - } - - FooterContainer( - footer = footerText, - modifier = modifier, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) - .padding( - vertical = TangemTheme.dimens.spacing16, - horizontal = TangemTheme.dimens.spacing20, - ), - ) { - Text( - text = stringResource(R.string.send_amount_substract), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding(end = TangemTheme.dimens.spacing12), - ) - TangemSwitch( - checked = isSubtract, - onCheckedChange = onSelectClick, - ) - } - } -} \ No newline at end of file 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 8267ee3c2a..4cbe0159d8 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 @@ -171,6 +171,7 @@ internal class SendViewModel @Inject constructor( private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var balanceJobHolder = JobHolder() + private var balanceHidingJobHolder = JobHolder() private var recipientsJobHolder = JobHolder() private var feeJobHolder = JobHolder() private var addressValidationJobHolder = JobHolder() @@ -180,25 +181,35 @@ internal class SendViewModel @Inject constructor( private var sendIdleTimer = 0L + init { + subscribeOnCurrencyStatusUpdates() + subscribeOnBalanceHidden() + } + override fun onCreate(owner: LifecycleOwner) { - subscribeOnCurrencyStatusUpdates(owner) onStateActive() - subscribeOnBalanceHidden(owner) analyticsEventHandler.send(SendAnalyticEvents.SendOpened) } + override fun onCleared() { + super.onCleared() + balanceHidingJobHolder.cancel() + balanceJobHolder.cancel() + stateRouter.clear() + } + fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) { innerRouter = router this.stateRouter = stateRouter } - private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) { + private fun subscribeOnCurrencyStatusUpdates() { viewModelScope.launch(dispatchers.main) { getUserWalletUseCase(userWalletId).fold( ifRight = { wallet -> userWallet = wallet checkIfSubtractAvailable() - getCurrenciesStatusUpdates(owner, wallet) + getCurrenciesStatusUpdates(wallet) }, ifLeft = { uiState = eventStateFactory.getGenericErrorState( @@ -210,23 +221,22 @@ internal class SendViewModel @Inject constructor( } } - private fun subscribeOnBalanceHidden(owner: LifecycleOwner) { + private fun subscribeOnBalanceHidden() { getBalanceHidingSettingsUseCase() - .flowWithLifecycle(owner.lifecycle) .conflate() .distinctUntilChanged() .onEach { uiState = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden) } .launchIn(viewModelScope) + .saveIn(balanceHidingJobHolder) } - private fun getCurrenciesStatusUpdates(owner: LifecycleOwner, wallet: UserWallet) { + private fun getCurrenciesStatusUpdates(wallet: UserWallet) { val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency if (cryptoCurrency is CryptoCurrency.Coin) { getCurrencyStatusUpdates(isSingleWallet = isSingleWallet) - .flowWithLifecycle(owner.lifecycle) .onEach { currencyStatus -> currencyStatus.onRight { onDataLoaded( @@ -249,7 +259,7 @@ internal class SendViewModel @Inject constructor( coinCurrencyStatus = coinStatus.getOrElse { error("Coin status is unreachable") }, ) } - }.flowWithLifecycle(owner.lifecycle) + } .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(balanceJobHolder) @@ -703,11 +713,10 @@ internal class SendViewModel @Inject constructor( } private fun onCheckFeeUpdate() { - val isSending = uiState.sendState.isSending val isSuccess = uiState.sendState.isSuccess val noErrorNotifications = uiState.sendState.notifications.none { it is SendNotification.Error } - if (!isSending && !isSuccess && noErrorNotifications) { + if (!isSuccess && noErrorNotifications) { viewModelScope.launch(dispatchers.main) { val feeUpdatedState = callFeeUseCase()?.fold( ifRight = { From 0a2b586b6e0f4e78be778233e9a42a602e72e179 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Feb 2024 15:28:48 +0300 Subject: [PATCH 33/33] Updated on 2026-08-14 --- .../java/com/tangem/core/ui/components/ResizableText.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt index 8801cb7983..e63aa1c8e3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt @@ -1,6 +1,7 @@ package com.tangem.core.ui.components import androidx.annotation.FloatRange +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material.LocalTextStyle import androidx.compose.material.Text import androidx.compose.runtime.* @@ -84,9 +85,11 @@ fun ResizableText( var readyToDraw by remember { mutableStateOf(value = false) } Text( - modifier = modifier.drawWithContent { - if (readyToDraw) drawContent() - }, + modifier = modifier + .drawWithContent { + if (readyToDraw) drawContent() + } + .wrapContentHeight(), text = text, color = color, fontSize = fontSize,