diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index cb7c080185..095b8f4ea0 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit cb7c080185a65b3b25cee30c331b696f784783f9 +Subproject commit 095b8f4ea0fa02e7ccea93cf0f437346345297ef diff --git a/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt new file mode 100644 index 0000000000..d1aecc29d0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt @@ -0,0 +1,31 @@ +package com.tangem.tap.common.analytics + +import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.utils.AnalyticsContextProxy +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.common.extensions.addContext +import com.tangem.tap.common.extensions.eraseContext +import com.tangem.tap.common.extensions.removeContext +import com.tangem.tap.common.extensions.setContext + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy { + + override fun setContext(scanResponse: ScanResponse) { + Analytics.setContext(scanResponse) + } + + override fun eraseContext() { + Analytics.eraseContext() + } + + override fun addContext(scanResponse: ScanResponse) { + Analytics.addContext(scanResponse) + } + + override fun removeContext() { + Analytics.removeContext() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt index 549f514b01..7fe3ef0081 100644 --- a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt @@ -1,8 +1,6 @@ package com.tangem.tap.common.url import android.content.Context -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.content.Intent.FLAG_ACTIVITY_NO_HISTORY import android.net.Uri import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent @@ -36,9 +34,6 @@ internal class CustomTabsUrlOpener : UrlOpener { ) .build() - // Open CustomTabsActivity as new task without saving into the stack - customTabsIntent.intent.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_NO_HISTORY) - customTabsIntent.launchUrl(context, Uri.parse(url)) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt index 34dbbd2e11..c687d63c10 100644 --- a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt @@ -1,6 +1,8 @@ package com.tangem.tap.di.analytics +import com.tangem.core.analytics.utils.AnalyticsContextProxy import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase +import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase import dagger.Module import dagger.Provides @@ -17,4 +19,8 @@ internal object AnalyticsModule { fun provideChangeCardAnalyticsContextUseCase(): ChangeCardAnalyticsContextUseCase { return DefaultChangeCardAnalyticsContextUseCase() } + + @Provides + @Singleton + fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index af14718da8..bebc27226f 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -184,30 +184,6 @@ internal object SettingsDomainModule { return NeverToInitiallyAskPermissionUseCase(repository = permissionRepository) } - @Provides - @Singleton - fun provideIsFirstTimeAskingPermissionUseCase( - permissionRepository: PermissionRepository, - ): IsFirstTimeAskingPermissionUseCase { - return IsFirstTimeAskingPermissionUseCase(repository = permissionRepository) - } - - @Provides - @Singleton - fun provideSetFirstTimeAskingPushPermissionUseCase( - permissionRepository: PermissionRepository, - ): SetFirstTimeAskingPermissionUseCase { - return SetFirstTimeAskingPermissionUseCase(repository = permissionRepository) - } - - @Provides - @Singleton - fun provideDelayPermissionRequestUseCase( - permissionRepository: PermissionRepository, - ): DelayPermissionRequestUseCase { - return DelayPermissionRequestUseCase(repository = permissionRepository) - } - @Provides @Singleton fun provideShouldAskPermissionUseCase(permissionRepository: PermissionRepository): ShouldAskPermissionUseCase { diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 3dfc085c10..cc9b89e775 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -1,12 +1,12 @@ package com.tangem.tap.di.domain import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* @@ -95,8 +95,11 @@ internal object WalletsDomainModule { @Provides @Singleton - fun providesRenameWalletUseCase(userWalletsListManager: UserWalletsListManager): RenameWalletUseCase { - return RenameWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesRenameWalletUseCase( + userWalletsListManager: UserWalletsListManager, + dispatchers: CoroutineDispatcherProvider, + ): RenameWalletUseCase { + return RenameWalletUseCase(userWalletsListManager = userWalletsListManager, dispatchers = dispatchers) } @Provides 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 0bbdc3f5f4..ea7c4ac652 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 @@ -32,6 +32,7 @@ import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext internal object LegacyScanProcessor { @@ -115,24 +116,28 @@ internal object LegacyScanProcessor { } else { scope.launch { delay(DELAY_SDK_DIALOG_CLOSE) - disclaimerWillShow() - store.dispatchWithMain( - DisclaimerAction.Show( - from = DisclaimerSource.Home, - callback = DisclaimerCallback( - onAccept = { - scope.launch(Dispatchers.Main) { - nextHandler(scanResponse) - } - }, - onDismiss = { - scope.launch(Dispatchers.Main) { - onFailure(TangemSdkError.UserCancelled()) - } - }, + + withContext(Dispatchers.Main.immediate) { + disclaimerWillShow() + + store.dispatch( + DisclaimerAction.Show( + from = DisclaimerSource.Home, + callback = DisclaimerCallback( + onAccept = { + scope.launch(Dispatchers.Main.immediate) { + nextHandler(scanResponse) + } + }, + onDismiss = { + scope.launch(Dispatchers.Main.immediate) { + onFailure(TangemSdkError.UserCancelled()) + } + }, + ), ), - ), - ) + ) + } } } } 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 01fe1e3deb..3e8529f150 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 @@ -176,9 +176,9 @@ class WalletConnectSdkHelper { is Result.Success -> gasLimitResult.data.toBigDecimal().multiply(BigDecimal("1.2")) is Result.Failure -> { (gasLimitResult.error as? Throwable)?.let { Timber.e(it, "getGasLimit failed") } - BigDecimal(DEFAULT_MAX_GASLIMIT) // Set high gasLimit if not provided + DEFAULT_MAX_GASLIMIT.toBigDecimal() // Set high gasLimit if not provided } - else -> BigDecimal(DEFAULT_MAX_GASLIMIT) // Set high gasLimit if not provided + else -> DEFAULT_MAX_GASLIMIT.toBigDecimal() // Set high gasLimit if not provided } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 4266b2fa73..cea126bf75 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -35,7 +35,8 @@ internal class DefaultLegacyWalletConnectRepository( private val _activeSessions: MutableSharedFlow> = MutableSharedFlow() override val activeSessions: Flow> = _activeSessions - private var currentSessions: List = emptyList() + override var currentSessions: List = emptyList() + private set /** * @param projectId Project ID at https://cloud.walletconnect.com/ diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt index 9096e879e0..616643a960 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt @@ -9,6 +9,8 @@ interface LegacyWalletConnectRepository { val activeSessions: Flow> + val currentSessions: List + fun init(projectId: String) fun setUserNamespaces(userNamespaces: Map>) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 66f8b5d16c..1f5dce9663 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -132,6 +132,7 @@ class WalletConnectInteractor( runCatching { if (accounts.isEmpty()) return isWalletConnectReadyForDeepLinks = true + if (deeplinkStack.empty()) return val lastDeeplink = deeplinkStack.pop() store.dispatchOnMain(WalletConnectAction.OpenSession(lastDeeplink)) }.onFailure { @@ -361,6 +362,19 @@ class WalletConnectInteractor( * @param deeplink deeplink to handle */ fun addDeeplink(deeplink: String) { + val deeplinkRegex = Regex(WC_PARAM_REGEX) + val matched = deeplinkRegex.findAll(deeplink) + val sessionTopic = matched.firstOrNull { it.value.contains(WC_TOPIC_QUERY_NAME) }?.groupValues?.lastOrNull() + + val isAlreadyActiveSessionTopic = walletConnectRepository.currentSessions.any { session -> + session.topic == sessionTopic + } + + if (isAlreadyActiveSessionTopic && sessionTopic != null) { + Timber.i("WC already has an active session topic: $deeplink") + return + } + if (isWalletConnectReadyForDeepLinks) { store.dispatchOnMain(WalletConnectAction.OpenSession(deeplink)) } else { @@ -406,7 +420,9 @@ class WalletConnectInteractor( return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId) } - companion object { - private const val WC_SCHEME = "wc" + private companion object { + const val WC_SCHEME = "wc" + const val WC_TOPIC_QUERY_NAME = "sessionTopic" + const val WC_PARAM_REGEX = "([a-zA-Z\\d-]+)=([a-zA-Z\\d]+)" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index 6054581056..328815f377 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -5,11 +5,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -25,12 +22,6 @@ internal class CardSettingsFragment : ComposeFragment() { override fun ScreenContent(modifier: Modifier) { val state by viewModel.screenState.collectAsStateWithLifecycle() - CardSettingsScreen( - modifier = modifier, - state = state, - onBackClick = { - store.dispatchNavigationAction(AppRouter::pop) - }, - ) + CardSettingsScreen(modifier = modifier, state = state) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index b0e2785b32..57f1103703 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -1,10 +1,13 @@ package com.tangem.tap.features.details.ui.cardsettings import android.content.res.Configuration -import androidx.compose.foundation.* +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -13,18 +16,14 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @Composable -internal fun CardSettingsScreen( - state: CardSettingsScreenState, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, -) { +internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifier = Modifier) { val needReadCard = state.cardDetails == null SettingsScreensScaffold( @@ -37,7 +36,7 @@ internal fun CardSettingsScreen( } }, titleRes = R.string.card_settings_title, - onBackClick = onBackClick, + onBackClick = state.onBackClick, ) } @@ -180,7 +179,7 @@ private fun CardSettings(state: CardSettingsScreenState) { // region Preview @Composable private fun CardSettingsScreenStateSample() { - CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}, onElementClick = {}), {}) + CardSettingsScreen(state = CardSettingsScreenState(onBackClick = {}, onScanCardClick = {}, onElementClick = {})) } @Preview(showBackground = true, widthDp = 360) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt index 46425706b4..bbde2c8579 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt @@ -12,6 +12,7 @@ internal data class CardSettingsScreenState( val cardDetails: List? = null, val onScanCardClick: () -> Unit, val onElementClick: (CardInfo) -> Unit, + val onBackClick: () -> Unit, ) internal sealed class CardInfo( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index d8b9f44ca9..7f972b948d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import com.tangem.common.CompletionResult import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.Analytics import com.tangem.domain.card.ScanCardProcessor @@ -23,12 +24,14 @@ import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.domain.extensions.signedHashesCount import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor import com.tangem.tap.features.details.ui.common.utils.* +import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode +import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.store import com.tangem.wallet.R import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -37,6 +40,7 @@ import javax.inject.Inject internal class CardSettingsViewModel @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val tangemSdkManager: TangemSdkManager, + private val cardSettingsInteractor: CardSettingsInteractor, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -44,24 +48,28 @@ internal class CardSettingsViewModel @Inject constructor( ?.unbundle(UserWalletId.serializer()) ?: error("User wallet ID is required for CardSettingsViewModel") - private val scannedScanResponse = MutableStateFlow(value = null) - val screenState: MutableStateFlow = MutableStateFlow(getInitialState()) + init { + cardSettingsInteractor.scannedScanResponse + .filterNotNull() + .onEach(::updateCardDetails) + .launchIn(viewModelScope) + } + private fun getInitialState() = CardSettingsScreenState( cardDetails = null, onElementClick = ::handleClickingItem, onScanCardClick = ::scanCard, + onBackClick = ::onBackClick, ) private fun scanCard() = viewModelScope.launch { scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) .doOnSuccess { scanResponse -> - scannedScanResponse.value = scanResponse - val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() if (userWalletId == scannedUserWalletId || scannedUserWalletId == null) { - updateCardDetails(scanResponse) + cardSettingsInteractor.initialize(scanResponse) } else { store.dispatchDialogShow( AppDialog.SimpleOkDialogRes( @@ -129,27 +137,8 @@ internal class CardSettingsViewModel @Inject constructor( changeAccessCode() } is CardInfo.ResetToFactorySettings -> { - val card = requireNotNull(scannedScanResponse.value) { - "Impossible to reset card if ScanResponse is null" - }.card - Analytics.send(Settings.CardSettings.ButtonFactoryReset()) - store.dispatchNavigationAction { - push( - route = AppRoute.ResetToFactory( - userWalletId = userWalletId, - cardId = card.cardId, - isActiveBackupStatus = card.backupStatus?.isActive == true, - backupCardsCount = when (val status = card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount - is CardDTO.BackupStatus.CardLinked, - CardDTO.BackupStatus.NoBackup, - null, - -> 0 - }, - ), - ) - } + resetWalletToFactorySettings() } is CardInfo.SecurityMode -> { Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode()) @@ -158,16 +147,47 @@ internal class CardSettingsViewModel @Inject constructor( } } is CardInfo.AccessCodeRecovery -> { - store.dispatchNavigationAction { - push(route = AppRoute.AccessCodeRecovery(userWalletId)) - } + store.dispatchNavigationAction { push(AppRoute.AccessCodeRecovery) } } else -> {} } } + private fun resetWalletToFactorySettings() { + val scanResponse = requireNotNull(cardSettingsInteractor.scannedScanResponse.value) { + "Impossible to reset card if ScanResponse is null" + } + + if (scanResponse.cardTypesResolver.isTangemTwins()) { + store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse))) + + cardSettingsInteractor.clear() + + store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) } + } else { + val card = scanResponse.card + + store.dispatchNavigationAction { + push( + route = AppRoute.ResetToFactory( + userWalletId = userWalletId, + cardId = card.cardId, + isActiveBackupStatus = card.backupStatus?.isActive == true, + backupCardsCount = when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + is CardDTO.BackupStatus.CardLinked, + CardDTO.BackupStatus.NoBackup, + null, + -> 0 + }, + ), + ) + } + } + } + private fun changeAccessCode() = viewModelScope.launch { - val scanResponse = requireNotNull(scannedScanResponse.value) { "Scan response is null" } + val scanResponse = requireNotNull(cardSettingsInteractor.scannedScanResponse.value) { "Scan response is null" } when (val result = tangemSdkManager.setAccessCode(scanResponse.card.cardId)) { is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged()) @@ -182,4 +202,9 @@ internal class CardSettingsViewModel @Inject constructor( val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin() return !isNotAllowed } + + private fun onBackClick() { + cardSettingsInteractor.clear() + store.dispatchNavigationAction(AppRouter::pop) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt index 8404af2beb..d65fb74a54 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt @@ -1,23 +1,16 @@ package com.tangem.tap.features.details.ui.cardsettings.coderecovery -import android.os.Bundle -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import arrow.core.getOrElse import com.tangem.common.doOnSuccess -import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.Analytics import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor import com.tangem.tap.features.details.ui.common.utils.isAccessCodeRecoveryEnabled import com.tangem.tap.store import dagger.hilt.android.lifecycle.HiltViewModel @@ -28,25 +21,21 @@ import javax.inject.Inject @HiltViewModel internal class AccessCodeRecoveryViewModel @Inject constructor( - private val getUserWalletUseCase: GetUserWalletUseCase, private val tangemSdkManager: TangemSdkManager, - savedStateHandle: SavedStateHandle, + private val cardSettingsInteractor: CardSettingsInteractor, ) : ViewModel() { - private val userWalletId = savedStateHandle.get(AppRoute.AccessCodeRecovery.USER_WALLET_ID_KEY) - ?.unbundle(UserWalletId.serializer()) - ?: error("UserWalletId is required for AccessCodeRecoveryViewModel") + private val scannedScanResponse = cardSettingsInteractor.scannedScanResponse.value + ?: error("Scan response is null") val screenState = MutableStateFlow( value = getInitialState(), ) private fun getInitialState(): AccessCodeRecoveryScreenState { - val userWallet = getUserWallet() - val isEnabled = isAccessCodeRecoveryEnabled( - typeResolver = userWallet.scanResponse.cardTypesResolver, - card = userWallet.scanResponse.card, + typeResolver = scannedScanResponse.cardTypesResolver, + card = scannedScanResponse.card, ) return AccessCodeRecoveryScreenState( @@ -59,11 +48,10 @@ internal class AccessCodeRecoveryViewModel @Inject constructor( } private fun saveChanges() = viewModelScope.launch { - val userWallet = getUserWallet() val isEnabled = screenState.value.enabledSelection tangemSdkManager - .setAccessCodeRecoveryEnabled(userWallet.cardId, isEnabled) + .setAccessCodeRecoveryEnabled(scannedScanResponse.card.cardId, isEnabled) .doOnSuccess { Analytics.send( Settings.CardSettings.AccessCodeRecoveryChanged( @@ -71,6 +59,16 @@ internal class AccessCodeRecoveryViewModel @Inject constructor( ), ) + cardSettingsInteractor.update { scanResponse -> + scanResponse.copy( + card = scanResponse.card.copy( + userSettings = scanResponse.card.userSettings?.copy( + isUserCodeRecoveryAllowed = isEnabled, + ), + ), + ) + } + store.dispatchNavigationAction(AppRouter::pop) } } @@ -78,14 +76,9 @@ internal class AccessCodeRecoveryViewModel @Inject constructor( private fun selectOption(isEnabled: Boolean) { screenState.update { it.copy( + enabledSelection = isEnabled, isSaveChangesEnabled = isEnabled != it.enabledOnCard, ) } } - - private fun getUserWallet(): UserWallet { - return getUserWalletUseCase(userWalletId).getOrElse { - error("Unable to get user wallet $userWalletId: $it") - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/domain/CardSettingsInteractor.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/domain/CardSettingsInteractor.kt new file mode 100644 index 0000000000..73bbf10991 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/domain/CardSettingsInteractor.kt @@ -0,0 +1,35 @@ +package com.tangem.tap.features.details.ui.cardsettings.domain + +import com.tangem.domain.models.scan.ScanResponse +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Interactor for sharing logic and data between all card settings screens + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class CardSettingsInteractor @Inject constructor() { + + private val _scannedScanResponse = MutableStateFlow(value = null) + val scannedScanResponse: StateFlow = _scannedScanResponse + + fun initialize(scanResponse: ScanResponse) { + _scannedScanResponse.value = scanResponse + } + + fun update(transform: (ScanResponse) -> ScanResponse) { + _scannedScanResponse.update { + requireNotNull(it) + transform(it) + } + } + + fun clear() { + _scannedScanResponse.value = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index b35c17a5fd..acafefe8e7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -60,7 +60,7 @@ internal sealed class SettingsItem( data class LinkMoreCards( override val onClick: () -> Unit, ) : SettingsItem( - iconResId = R.drawable.ic_more_cards, + iconResId = R.drawable.ic_more_cards_24, title = resourceReference(R.string.details_row_title_create_backup), ) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index 41d0535147..fefbdf122e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -23,6 +23,7 @@ import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.features.details.redux.ResetCardDialog +import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor import com.tangem.tap.features.details.ui.common.utils.getResetToFactoryDescription import com.tangem.tap.store import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE @@ -44,6 +45,7 @@ internal class ResetCardViewModel @Inject constructor( private val deleteWalletUseCase: DeleteWalletUseCase, private val userWalletsListManager: UserWalletsListManager, private val analyticsEventHandler: AnalyticsEventHandler, + private val cardSettingsInteractor: CardSettingsInteractor, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -255,6 +257,8 @@ internal class ResetCardViewModel @Inject constructor( } private fun finishFullReset() { + cardSettingsInteractor.clear() + val newSelectedWallet = userWalletsListManager.selectedUserWalletSync if (newSelectedWallet != null) { 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 f92bc4be2f..9cda07b754 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 @@ -6,14 +6,13 @@ import android.net.Uri [REDACTED_AUTHOR] */ interface Disclaimer { - fun type(): DisclaimerType fun getUri(): Uri suspend fun accept() suspend fun isAccepted(): Boolean } abstract class BaseDisclaimer( - protected val dataProvider: DisclaimerDataProvider, + private val dataProvider: DisclaimerDataProvider, ) : Disclaimer { val baseUrl = "https://tangem.com" @@ -26,46 +25,11 @@ abstract class BaseDisclaimer( } class DummyDisclaimer : Disclaimer { - override fun type(): DisclaimerType = DisclaimerType.Tangem override fun getUri(): Uri = Uri.parse("https://tangem.com/tangem_tos.html") 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") -} - -class Start2CoinDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) { - override fun type(): DisclaimerType = DisclaimerType.Start2Coin - override fun getUri(): Uri = Uri.parse("$baseUrl/" + filename(dataProvider.getLanguage(), getRegion())) - - @Suppress("ComplexMethod") - private fun filename(languageCode: String, regionCode: String?): String { - return when { - languageCode == "fr" && regionCode == "ch" -> "start2coin-fr-ch-tangem.html" - languageCode == "de" && regionCode == "ch" -> "start2coin-de-ch-tangem.html" - languageCode == "en" && regionCode == "ch" -> "start2coin-en-ch-tangem.html" - languageCode == "it" && regionCode == "ch" -> "start2coin-it-ch-tangem.html" - languageCode == "fr" && regionCode == "fr" -> "start2coin-fr-fr-tangem.html" - languageCode == "de" && regionCode == "at" -> "start2coin-de-at-tangem.html" - regionCode == "fr" -> "start2coin-fr-fr-tangem.html" - regionCode == "ch" -> "start2coin-en-ch-tangem.html" - regionCode == "at" -> "start2coin-de-at-tangem.html" - else -> "start2coin-fr-fr-tangem.html" - } - } - - private fun getRegion(): String? { - val cardId = dataProvider.getCardId() - 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/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt index 796452dcab..90f73cb4ad 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 @@ -1,58 +1,28 @@ 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 -/** -[REDACTED_AUTHOR] - */ -enum class DisclaimerType { - Tangem, - Start2Coin, - ; - - companion object { - fun get(cardDTO: CardDTO): DisclaimerType { - return when { - cardDTO.isStart2Coin -> Start2Coin - else -> Tangem - } - } - } +fun CardDTO.createDisclaimer(): Disclaimer { + val dataProvider = provideDisclaimerDataProvider(cardId) + return TangemDisclaimer(dataProvider) } -fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer { - val dataProvider = provideDisclaimerDataProvider(cardDTO.cardId, this) - return when (this) { - DisclaimerType.Tangem -> TangemDisclaimer(dataProvider) - DisclaimerType.Start2Coin -> Start2CoinDisclaimer(dataProvider) - } -} - -fun CardDTO.createDisclaimer(): Disclaimer = DisclaimerType.get(this).createDisclaimer(this) - -private fun provideDisclaimerDataProvider(cardId: String, disclaimerType: DisclaimerType): DisclaimerDataProvider { +private fun provideDisclaimerDataProvider(cardId: String): DisclaimerDataProvider { val cardRepository = store.inject(DaggerGraphState::cardRepository) return object : DisclaimerDataProvider { override fun getLanguage(): String = Locale.getDefault().language override fun getCardId(): String = cardId override suspend fun accept() { - when (disclaimerType) { - DisclaimerType.Tangem -> cardRepository.acceptTangemTOS() - DisclaimerType.Start2Coin -> cardRepository.acceptStart2CoinTOS(cardId) - } + cardRepository.acceptTangemTOS() } override suspend fun isAccepted(): Boolean { - return when (disclaimerType) { - DisclaimerType.Tangem -> cardRepository.isTangemTOSAccepted() - DisclaimerType.Start2Coin -> cardRepository.isStart2CoinTOSAccepted(cardId) - } + return cardRepository.isTangemTOSAccepted() } } } \ No newline at end of file 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 a2a3757628..9da71e64e8 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 @@ -1,5 +1,7 @@ package com.tangem.tap.features.home.redux +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase import com.tangem.common.doOnFailure import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess @@ -61,8 +63,13 @@ private fun handleHomeAction(action: Action) { } is HomeAction.GoToShop -> { Analytics.send(Shop.ScreenOpened()) - store.dispatchOpenUrl(NEW_BUY_WALLET_URL) - + Firebase.analytics.appInstanceId + .addOnSuccessListener { + store.dispatchOpenUrl("$NEW_BUY_WALLET_URL&app_instance_id=$it") + } + .addOnFailureListener { + store.dispatchOpenUrl(NEW_BUY_WALLET_URL) + } // disabled for now in task [REDACTED_JIRA] // when (action.userCountryCode) { // RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL) 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 d6c4adff57..404a469233 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 @@ -32,26 +32,27 @@ import timber.log.Timber */ object OnboardingHelper { suspend fun isOnboardingCase(response: ScanResponse): Boolean { - val onboardingManager = store.state.globalState.onboardingState.onboardingManager + val onboardingManager = + store.state.globalState.onboardingState.onboardingManager ?: OnboardingManager(response) val cardId = response.card.cardId return when { response.cardTypesResolver.isTangemTwins() -> { if (!response.twinsIsTwinned()) { true } else { - onboardingManager?.isActivationInProgress(cardId) ?: false + onboardingManager.isActivationInProgress(cardId) ?: false } } response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> { val emptyWallets = response.card.wallets.isEmpty() - val activationInProgress = onboardingManager?.isActivationInProgress(cardId) + val activationInProgress = onboardingManager.isActivationInProgress(cardId) val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup && !DemoHelper.isDemoCard(response) - emptyWallets || activationInProgress == true || isNoBackup + emptyWallets || activationInProgress || isNoBackup } - response.card.wallets.isNotEmpty() -> onboardingManager?.isActivationInProgress(cardId) ?: false + response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId) ?: false else -> true } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt index 7ac3806126..757ec91454 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt @@ -11,12 +11,14 @@ import coil.load import com.tangem.blockchain.common.Blockchain import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.ShareElement +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.getDrawableCompat import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget import com.tangem.tap.common.transitions.InternalNoteLayoutTransition import com.tangem.tap.features.addBackPressHandler +import com.tangem.tap.features.onboarding.OnboardingWalletBalance import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState @@ -134,7 +136,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } private fun setupTopUpWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { - if (state.isBuyAllowed) { + if (availableForBuy(state.scanResponse, state.walletBalance)) { btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) btnMainAction.icon = null btnMainAction.setOnClickListener { @@ -217,6 +219,11 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } } + private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean { + scanResponse ?: return false + return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency) + } + override fun handleOnBackPressed() { store.dispatch(OnboardingNoteAction.OnBackPressed) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt index 5552fc8b50..14f7989391 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt @@ -13,7 +13,7 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingNoteSt when (action) { is GlobalAction.Onboarding.Start -> { - state = OnboardingNoteState() + state = OnboardingNoteState(scanResponse = action.scanResponse) } is OnboardingNoteAction.SetArtworkUrl -> { state = state.copy(cardArtworkUrl = action.artworkUrl) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt index ae7a2db594..3d659e39da 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt @@ -1,11 +1,10 @@ package com.tangem.tap.features.onboarding.products.note.redux import com.tangem.blockchain.common.WalletManager +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TapError import com.tangem.tap.features.onboarding.OnboardingWalletBalance -import com.tangem.tap.store import org.rekotlin.StateType -import kotlin.properties.ReadOnlyProperty /** [REDACTED_AUTHOR] @@ -20,14 +19,11 @@ data class OnboardingNoteState( val currentStep: OnboardingNoteStep = OnboardingNoteStep.None, val steps: List = OnboardingNoteStep.values().toList(), val showConfetti: Boolean = false, + val scanResponse: ScanResponse? = null, ) : StateType { val progress: Int get() = steps.indexOf(currentStep) - - val isBuyAllowed: Boolean by ReadOnlyProperty { _, _ -> - store.state.globalState.exchangeManager.availableForBuy(walletBalance.currency) - } } enum class OnboardingNoteStep { 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 2bc6f07ce1..19d77c509e 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 @@ -46,7 +46,7 @@ object TwinCardsMiddleware { val handler = twinsWalletMiddleware } -private val twinsWalletMiddleware: Middleware = { dispatch, state -> +private val twinsWalletMiddleware: Middleware = { dispatch, _ -> { next -> { action -> handle(action, dispatch) @@ -66,15 +66,15 @@ private fun handle(action: Action, dispatch: DispatchFunction) { fun getScanResponse(): ScanResponse { return when (twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse - CreateTwinWalletMode.RecreateWallet -> globalState.scanResponse + is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse + is CreateTwinWalletMode.RecreateWallet -> globalState.scanResponse } ?: throw NullPointerException("ScanResponse can't be NULL") } fun updateScanResponse(response: ScanResponse) { when (twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response - CreateTwinWalletMode.RecreateWallet -> store.dispatchOnMain(GlobalAction.SaveScanResponse(response)) + is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response + is CreateTwinWalletMode.RecreateWallet -> store.dispatchOnMain(GlobalAction.SaveScanResponse(response)) } } @@ -105,6 +105,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) { mainScope.launch { if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return@launch + if (twinCardsState.mode is CreateTwinWalletMode.RecreateWallet) { + store.dispatch(GlobalAction.SaveScanResponse(twinCardsState.mode.scanResponse)) + } + val scanResponse = getScanResponse() onboardingManager?.apply { if (!isActivationStarted(scanResponse.card.cardId)) { @@ -113,7 +117,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } when (twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> { + is CreateTwinWalletMode.CreateWallet -> { mainScope.launch { val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) .invokeSync() @@ -132,7 +136,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { store.dispatch(dispatchAction) } } - CreateTwinWalletMode.RecreateWallet -> { + is CreateTwinWalletMode.RecreateWallet -> { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning)) } } @@ -226,10 +230,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) { delay(DELAY_SDK_DIALOG_CLOSE) withMainContext { when (twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> { + is CreateTwinWalletMode.CreateWallet -> { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.TopUpWallet)) } - CreateTwinWalletMode.RecreateWallet -> { + is CreateTwinWalletMode.RecreateWallet -> { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done)) } } @@ -319,11 +323,11 @@ private fun handle(action: Action, dispatch: DispatchFunction) { TwinCardsAction.Done -> { val scanResponse = getScanResponse() when (twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> { + is CreateTwinWalletMode.CreateWallet -> { store.dispatchOnMain(GlobalAction.Onboarding.Stop) OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) } - CreateTwinWalletMode.RecreateWallet -> { + is CreateTwinWalletMode.RecreateWallet -> { scope.launch { val walletsRepository = store.inject(DaggerGraphState::walletsRepository) @@ -368,8 +372,7 @@ private fun getPopBackScreen(): KClass { val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) return if (userWalletsListManager.hasUserWallets) { - val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync } - .fold(onSuccess = { true }, onFailure = { false }) + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } if (isLocked) { AppRoute.Welcome::class diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt index fcc38388ac..83e641bc8a 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt @@ -6,9 +6,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TapError import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.onboarding.OnboardingWalletBalance -import com.tangem.tap.store import org.rekotlin.StateType -import kotlin.properties.ReadOnlyProperty /** [REDACTED_AUTHOR] @@ -34,7 +32,7 @@ data class TwinCardsState( val steps: List get() = when (mode) { - CreateTwinWalletMode.CreateWallet -> listOf( + is CreateTwinWalletMode.CreateWallet -> listOf( TwinCardsStep.None, TwinCardsStep.CreateFirstWallet, TwinCardsStep.CreateSecondWallet, @@ -42,7 +40,7 @@ data class TwinCardsState( TwinCardsStep.TopUpWallet, TwinCardsStep.Done, ) - CreateTwinWalletMode.RecreateWallet -> listOf( + is CreateTwinWalletMode.RecreateWallet -> listOf( TwinCardsStep.None, TwinCardsStep.CreateFirstWallet, TwinCardsStep.CreateSecondWallet, @@ -56,13 +54,15 @@ data class TwinCardsState( val twinningInProgress: Boolean get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet - - val isBuyAllowed: Boolean by ReadOnlyProperty { _, _ -> - store.state.globalState.exchangeManager.availableForBuy(walletBalance.currency) - } } -enum class CreateTwinWalletMode { CreateWallet, RecreateWallet } +sealed class CreateTwinWalletMode { + data object CreateWallet : CreateTwinWalletMode() + + data class RecreateWallet( + val scanResponse: ScanResponse, + ) : CreateTwinWalletMode() +} sealed class TwinCardsStep { object None : TwinCardsStep() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt index e1e0a58536..eda118c3dc 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt @@ -27,6 +27,7 @@ import com.tangem.tap.common.transitions.InternalNoteLayoutTransition import com.tangem.tap.domain.twins.TwinsCardWidget import com.tangem.tap.features.addBackPressHandler import com.tangem.tap.features.onboarding.OnboardingMenuProvider +import com.tangem.tap.features.onboarding.OnboardingWalletBalance import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction @@ -55,22 +56,17 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( override fun configureTransitions() { when (store.state.twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> { + is CreateTwinWalletMode.CreateWallet -> { super.configureTransitions() } - CreateTwinWalletMode.RecreateWallet -> { + is CreateTwinWalletMode.RecreateWallet -> { configureDefaultTransactions() } } } override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider( - scanResponseProvider = Provider { - store.state.twinCardsState.welcomeOnlyScanResponse - ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse - ?: store.state.detailsState.scanResponse - ?: error("ScanResponse must be not null") - }, + scanResponseProvider = Provider { getActualScanResponse() }, ) @Suppress("MagicNumber") @@ -342,7 +338,7 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( else -> {} } - if (state.isBuyAllowed) { + if (availableForBuy(getActualScanResponse(), state.walletBalance)) { btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) btnMainAction.setOnClickListener { store.dispatch(TwinCardsAction.TopUp) @@ -395,8 +391,8 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( tvBody.setText(R.string.onboarding_done_body) val layout = when (state.mode) { - CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins - CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done + is CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins + is CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done } updateConstraints(state.currentStep, layout) } @@ -424,6 +420,18 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( } } + private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean { + scanResponse ?: return false + return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency) + } + + private fun getActualScanResponse(): ScanResponse { + return store.state.twinCardsState.welcomeOnlyScanResponse + ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: store.state.detailsState.scanResponse + ?: error("ScanResponse must be not null") + } + override fun handleOnBackPressed() { store.dispatch( TwinCardsAction.OnBackPressed { should, popAction -> 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 c86ed79d1d..0e32a392c4 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 @@ -35,6 +35,7 @@ import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.wallet.R +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware @@ -44,6 +45,8 @@ object OnboardingWalletMiddleware { val handler = onboardingWalletMiddleware } +private const val HIDE_PROGRESS_DELAY = 400L + private val onboardingWalletMiddleware: Middleware = { dispatch, state -> { next -> { action -> @@ -161,17 +164,13 @@ private fun handleWalletAction(action: Action) { store.dispatch(GlobalAction.Onboarding.Stop) if (scanResponse == null) { - store.dispatchNavigationAction(AppRouter::pop) - store.dispatch(HomeAction.ReadCard(scope = action.scope)) + action.scope.launch { + readCard { newScanResponse -> + handleFinishOnboardind(newScanResponse) + } + } } else { - val backupState = store.state.onboardingWalletState.backupState - val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState) - OnboardingHelper.trySaveWalletAndNavigateToWalletScreen( - scanResponse = updatedScanResponse, - accessCode = backupState.accessCode, - backupCardsIds = backupState.backupCardIds, - hasBackupError = backupState.hasBackupError, - ) + handleFinishOnboardind(scanResponse) } } is OnboardingWalletAction.ResumeBackup -> { @@ -196,6 +195,43 @@ private fun handleWalletAction(action: Action) { } } +private fun handleFinishOnboardind(scanResponse: ScanResponse) { + val backupState = store.state.onboardingWalletState.backupState + val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState) + OnboardingHelper.trySaveWalletAndNavigateToWalletScreen( + scanResponse = updatedScanResponse, + accessCode = backupState.accessCode, + backupCardsIds = backupState.backupCardIds, + hasBackupError = backupState.hasBackupError, + ) +} + +private suspend fun readCard(onSuccess: (ScanResponse) -> Unit) { + val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + store.inject(DaggerGraphState::scanCardProcessor).scan( + analyticsSource = com.tangem.core.analytics.models.AnalyticsParam.ScreensSources.Intro, + onProgressStateChange = { showProgress -> + if (showProgress) { + store.dispatch(HomeAction.ScanInProgress(scanInProgress = true)) + } else { + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) + } + }, + onFailure = { + Timber.e(it, "Unable to scan card") + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) + }, + onSuccess = onSuccess, + ) +} + private suspend fun loadArtworkForUnfinishedBackup( cardId: String, cardPublicKey: ByteArray, diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt index c7679a466d..4206df1904 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt @@ -2,13 +2,7 @@ package com.tangem.tap.features.saveWallet.ui.components import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.* import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text @@ -20,16 +14,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.SpacerHHalf -import com.tangem.core.ui.components.SpacerW24 -import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.atoms.Hand -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.wallet.R @Composable @@ -37,10 +25,7 @@ internal fun SaveWalletScreenContent(showProgress: Boolean, onSaveWalletClick: ( Column(horizontalAlignment = Alignment.CenterHorizontally) { Header(onCloseClick = onCloseClick) SpacerHHalf() - Title( - modifier = Modifier - .widthIn(max = TangemTheme.dimens.size200), - ) + Title(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing22)) SpacerH32() Description( modifier = Modifier diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt index e122327ff8..4091e53c06 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt @@ -4,7 +4,10 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.Text @@ -123,14 +126,14 @@ internal fun HasMoreItem(moreCount: Int) { ) { Text( modifier = Modifier - .padding(TangemTheme.dimens.spacing4) .align(Alignment.Center) .drawWithContent { if (readyToDraw) drawContent() }, text = "+$count", style = textStyle, + color = TangemTheme.colors.text.tertiary, overflow = TextOverflow.Clip, onTextLayout = { textLayoutResult -> - if (textLayoutResult.didOverflowHeight) { + if (textLayoutResult.hasVisualOverflow) { textStyle = textStyle.copy(fontSize = textStyle.fontSize * 0.9) } else { readyToDraw = true diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt index e6c523e666..a8861eed2f 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt @@ -1,5 +1,6 @@ package com.tangem.tap.network.exchangeServices +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService @@ -32,7 +33,8 @@ internal class BuyExchangeService( override fun isSellAllowed(): Boolean = currentService.isSellAllowed() - override fun availableForBuy(currency: Currency): Boolean = currentService.availableForBuy(currency) + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = + currentService.availableForBuy(scanResponse, currency) override fun availableForSell(currency: Currency): Boolean = currentService.availableForSell(currency) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt index 0f4fd42f7d..696f9288bc 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt @@ -2,6 +2,7 @@ package com.tangem.tap.network.exchangeServices import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.model.Currency import com.tangem.tap.features.demo.isDemoCard @@ -38,8 +39,8 @@ class CardExchangeRules( } } - override fun availableForBuy(currency: Currency): Boolean { - val card = cardProvider() ?: return false + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean { + val card = scanResponse.card return when { card.isDemoCard() -> true 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 e962f7ebf4..fb01befe82 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.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.safeUpdate @@ -38,8 +39,9 @@ class CurrencyExchangeManager( override fun isBuyAllowed(): Boolean = primaryRules.isBuyAllowed() && buyService.isBuyAllowed() override fun isSellAllowed(): Boolean = primaryRules.isSellAllowed() && sellService.isSellAllowed() - override fun availableForBuy(currency: Currency): Boolean { - return primaryRules.availableForBuy(currency) && buyService.availableForBuy(currency) + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean { + return primaryRules.availableForBuy(scanResponse, currency) && + buyService.availableForBuy(scanResponse, currency) } override fun availableForSell(currency: Currency): Boolean { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 76de7375da..904e22c80e 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -1,13 +1,15 @@ package com.tangem.tap.network.exchangeServices import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager { private val cryptoCurrencyConverter = CryptoCurrencyConverter() - override fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean { + override fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean { return exchangeService?.availableForBuy( + scanResponse, currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), ) ?: false } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt index e9b17488e3..1cfd8502ed 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt @@ -1,5 +1,6 @@ package com.tangem.tap.network.exchangeServices +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.feature.Feature import com.tangem.tap.domain.model.Currency @@ -7,7 +8,7 @@ import com.tangem.tap.domain.model.Currency interface Exchanger { fun isBuyAllowed(): Boolean fun isSellAllowed(): Boolean - fun availableForBuy(currency: Currency): Boolean + fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean fun availableForSell(currency: Currency): Boolean } @@ -20,7 +21,7 @@ interface ExchangeService : Feature, Exchanger, ExchangeUrlBuilder { override suspend fun update() {} override fun isBuyAllowed(): Boolean = false override fun isSellAllowed(): Boolean = false - override fun availableForBuy(currency: Currency): Boolean = false + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean = false override fun getUrl( action: CurrencyExchangeManager.Action, @@ -45,7 +46,7 @@ interface ExchangeRules : Feature, Exchanger { override fun featureIsSwitchedOn(): Boolean = false override fun isBuyAllowed(): Boolean = false override fun isSellAllowed(): Boolean = false - override fun availableForBuy(currency: Currency): Boolean = false + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean = false } } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index d653e2babe..8bcc8168e9 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -6,6 +6,7 @@ import com.tangem.common.extensions.calculateSha512 import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.common.services.performRequest +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager @@ -28,7 +29,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E override fun isSellAllowed(): Boolean = false - override fun availableForBuy(currency: Currency): Boolean { + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean { if (!isBuyAllowed()) return false val mercuryoNetwork = currency.blockchain.mercuryoNetwork() diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index c004c38b21..de34810420 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -7,6 +7,7 @@ import com.tangem.common.services.Result import com.tangem.common.services.performRequest import com.tangem.datasource.api.common.createRetrofitInstance import com.tangem.domain.common.extensions.withIOContext +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager @@ -83,7 +84,7 @@ class MoonPayService( return status?.responseUserStatus?.isSellAllowed ?: false } - override fun availableForBuy(currency: Currency): Boolean = false + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean { if (!isSellAllowed()) return false 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 bdd1dbb86a..29b6c4b9d5 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -39,7 +39,7 @@ class TransactionManagerImpl( @Throws(IllegalStateException::class) override suspend fun getFee( networkId: String, - amountToSend: BigDecimal, + amountToSend: Amount, currencyToSend: Currency, destinationAddress: String, increaseBy: Int?, @@ -52,7 +52,7 @@ class TransactionManagerImpl( if (walletManager is EthereumOptimisticRollupWalletManager) { return getFeeForOptimismBlockchain( walletManager = walletManager, - amount = createAmount(amountToSend, currencyToSend, blockchain), + amount = amountToSend, destinationAddress = destinationAddress, data = data, ) @@ -61,7 +61,6 @@ class TransactionManagerImpl( walletManager = walletManager, blockchain = blockchain, amountToSend = amountToSend, - currency = currencyToSend, destinationAddress = destinationAddress, data = data, increaseBy = increaseBy, @@ -70,8 +69,6 @@ class TransactionManagerImpl( return getFeeForBlockchain( walletManager = walletManager, amountToSend = amountToSend, - currency = currencyToSend, - blockchain = blockchain, destinationAddress = destinationAddress, ) } @@ -87,13 +84,11 @@ class TransactionManagerImpl( private suspend fun getFeeForBlockchain( walletManager: WalletManager, - amountToSend: BigDecimal, - currency: Currency, - blockchain: Blockchain, + amountToSend: Amount, destinationAddress: String, ): ProxyFees { val fee = (walletManager as? TransactionSender)?.getFee( - amount = createAmount(amountToSend, currency, blockchain), + amount = amountToSend, destination = destinationAddress, ) ?: error("Cannot cast to TransactionSender") return when (fee) { @@ -155,17 +150,14 @@ class TransactionManagerImpl( private suspend fun getFeeForEthereumBlockchain( walletManager: EthereumWalletManager, blockchain: Blockchain, - amountToSend: BigDecimal, - currency: Currency, + amountToSend: Amount, destinationAddress: String, data: String?, increaseBy: Int?, ): ProxyFees { val gasLimit = getGasLimit( evmWalletManager = walletManager, - blockchain = blockchain, amount = amountToSend, - currency = currency, destinationAddress = destinationAddress, data = data, ).increaseBigIntegerByPercents(increaseBy) @@ -219,23 +211,20 @@ class TransactionManagerImpl( } } - @Suppress("LongParameterList") private suspend fun getGasLimit( evmWalletManager: EthereumWalletManager, - blockchain: Blockchain, - amount: BigDecimal, - currency: Currency, + amount: Amount, destinationAddress: String, data: String?, ): BigInteger { val result = if (data.isNullOrEmpty()) { evmWalletManager.getGasLimit( - amount = createAmount(amount, currency, blockchain), + amount = amount, destination = destinationAddress, ) } else { evmWalletManager.getGasLimit( - amount = createAmount(amount, currency, blockchain), + amount = amount, destination = destinationAddress, data = data, ) @@ -250,6 +239,18 @@ class TransactionManagerImpl( } } + override suspend fun getFeeForGas(networkId: String, gas: BigInteger, derivationPath: String?): ProxyFees { + val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + val walletManager = getActualWalletManager(blockchain, derivationPath) + val gasPriceResult = (walletManager as? EthereumWalletManager)?.getGasPrice() + ?: error("not supported for $blockchain") + val gasPrice = when (gasPriceResult) { + is Result.Failure -> error("fail to receive gasPrice") + is Result.Success -> gasPriceResult.data + } + return createMultipleProxyFees(gasPrice, gas, blockchain) + } + private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { val selectedUserWallet = requireNotNull( userWalletsListManager.selectedUserWalletSync, @@ -316,27 +317,6 @@ class TransactionManagerImpl( ) } - private fun createAmount(amount: BigDecimal, currency: Currency, blockchain: Blockchain): Amount { - return when (currency) { - is Currency.NativeToken -> { - Amount(value = amount, blockchain = blockchain) - } - is Currency.NonNativeToken -> { - Amount(convertNonNativeToken(currency), amount) - } - } - } - - private fun convertNonNativeToken(token: Currency.NonNativeToken): Token { - return Token( - name = token.name, - symbol = token.symbol, - contractAddress = token.contractAddress, - decimals = token.decimalCount, - id = token.id, - ) - } - private fun convertToProxyAmount(amount: Amount): ProxyAmount { return ProxyAmount( currencySymbol = amount.currencySymbol, diff --git a/app/src/main/res/drawable/ic_more_cards.xml b/app/src/main/res/drawable/ic_more_cards.xml deleted file mode 100644 index 0cc559021b..0000000000 --- a/app/src/main/res/drawable/ic_more_cards.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 38bff0851c..6c03778967 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -35,14 +35,7 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Disclaimer( val isTosAccepted: Boolean, - ) : AppRoute(path = "/disclaimer${if (isTosAccepted) "/tos_accepted" else ""}"), RouteBundleParams { - - override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - const val IS_TOS_ACCEPTED_KEY = "isTosAccepted" - } - } + ) : AppRoute(path = "/disclaimer${if (isTosAccepted) "/tos_accepted" else ""}") @Serializable data object OnboardingNote : AppRoute(path = "/onboarding/note") @@ -177,15 +170,9 @@ sealed class AppRoute(val path: String) : Route { } @Serializable - data class AccessCodeRecovery( - val userWalletId: UserWalletId, - ) : AppRoute(path = "/access_code_recovery/${userWalletId.stringValue}"), RouteBundleParams { + data object AccessCodeRecovery : AppRoute(path = "/access_code_recovery"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - const val USER_WALLET_ID_KEY = "userWalletId" - } } @Serializable diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index 9d05970191..c61c8e7af2 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /** Domain */ implementation(projects.domain.analytics) + implementation(projects.domain.models) /** Other */ implementation(deps.kotlin.coroutines) diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt b/core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt new file mode 100644 index 0000000000..cab4da6a7b --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt @@ -0,0 +1,17 @@ +package com.tangem.core.analytics.utils + +import com.tangem.domain.models.scan.ScanResponse + +/** +[REDACTED_AUTHOR] + */ +interface AnalyticsContextProxy { + + fun setContext(scanResponse: ScanResponse) + + fun eraseContext() + + fun addContext(scanResponse: ScanResponse) + + fun removeContext() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt index 7496a0a8e2..5e1eb6d5d3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt @@ -1,7 +1,6 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json -import java.math.BigDecimal data class ExchangeDataResponseWithTxDetails( val dataResponse: ExchangeDataResponse, @@ -50,7 +49,10 @@ data class TxDetails( val txData: String?, // transaction data if DEX, null if CEX @Json(name = "txValue") - val txValue: BigDecimal, // amount (same as fromAmount) + val txValue: String, // amount (same as fromAmount for Coin, but for bridge equal to otherNativeFee) + + @Json(name = "otherNativeFee") + val otherNativeFee: String?, @Json(name = "externalTxId") val externalTxId: String?, // null if DEX, provider transaction id if CEX @@ -63,6 +65,9 @@ data class TxDetails( @Json(name = "txExtraId") val txExtraId: String?, + + @Json(name = "gas") + val gas: String?, ) enum class TxType { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt index dc786b02de..7682c70d4f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt @@ -7,17 +7,23 @@ data class ExchangeStatusResponse( @Json(name = "providerId") val providerId: String, - @Json(name = "externalTxId") - val externalTxId: String, - @Json(name = "status") val status: ExchangeStatus, + @Json(name = "externalTxId") + val externalTxId: String?, + @Json(name = "externalTxUrl") - val externalTxUrl: String, + val externalTxUrl: String?, @Json(name = "error") val error: ExchangeStatusError?, + + @Json(name = "refundNetwork") + val refundNetwork: String? = null, + + @Json(name = "refundContractAddress") + val refundContractAddress: String? = null, ) enum class ExchangeStatus { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index eb9e2a434c..9fe41bf3a5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -18,6 +18,7 @@ interface TangemTechApi { @Query("contractAddress") contractAddress: String? = null, @Query("exchangeable") exchangeable: Boolean? = null, @Query("networkIds") networkIds: String? = null, + @Query("networkId") networkId: String? = null, @Query("active") active: Boolean? = null, @Query("searchText") searchText: String? = null, @Query("offset") offset: Int? = null, 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 c517d9fdec..bb2c5fa1b0 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 @@ -95,8 +95,6 @@ object PreferencesKeys { booleanPreferencesKey(name = "isTokenSwapPromoOkxShown") } - fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region") - // region Permission fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt index 2bcb9a3dbb..cf99b7c4ac 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt @@ -10,9 +10,12 @@ interface SwapTransactionStatusStore { } enum class ExchangeAnalyticsStatus(val value: String) { + WaitingTxHash("Waiting tx hash"), InProgress("In Progress"), Done("Done"), Fail("Fail"), + FailTx("Fail tx"), + Unknown("Unknown"), KYC("KYC"), Refunded("Refunded"), Cancelled("Canceled"), diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 500bfaa797..0123a1dfdd 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,7 +1,7 @@ Netzwerk wählen - Benutzerdefiniertes Token hinzufügen + Benutzerdef. Token hinzuf. Token verwalten Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen. So scannt man @@ -23,7 +23,7 @@ Durch das Entfernen der gespeicherten Karte werden alle gespeicherten Wallets und deren Zugangscodes aus der App gelöscht. Zugangscode speichern Bei Interaktionen mit deiner Karte wird anstelle des Zugangscodes eine biometrische Authentifizierung abgefragt. - Behalten die Wallet in der App + Behalte die Wallet in der App Aktiviere die Verknüpfung aller Wallets mit der Tangem-App. Die biometrische Authentifizierung ist zum Entsperren der App erforderlich. Das Signieren von Transaktionen erfordert das Antippen deiner Tangem-Karte. Dunkel Hell @@ -46,7 +46,7 @@ Deaktiviere diese Option, wenn du nicht möchtest, dass diese Karte zum Zurücksetzen von Zugangscodes auf anderen Karten in dieser Wallets verwendet wird. Bitte beachte, dass du dann auch den Zugangscode auf dieser Karte nicht zurücksetzen kannst. Ermöglicht die Verwendung dieser Karte zum Zurücksetzen des Zugangscodes auf anderen Karten in dieser Brieftasche - Wiederherstellung des Zugangscodes + Zugangscodes wiederherstellen Zurücksetzen Möchtest du das wirklich tun? Zugangscode ändern @@ -84,14 +84,14 @@ Abbrechen Stakingbelohnungen beanstpruchen Schließen - Weitermachen + Weiter Kopieren Adresse kopieren Erstellen - Benutzerdefiniert + Benutzerdef. - Tag - Tage + %d tag + %d tage Entfernen Deaktiviert @@ -107,9 +107,10 @@ Schnell Markt Langsam - Geschwindigkeit und Gebühr + Gebühren Adressen abrufen Zum Anbieter gehen + Zum Token Importieren Später Gesperrt @@ -153,7 +154,7 @@ Heute Transaktion fehlgeschlagen Transaktionen - Überweisen + Überweisung Ich verstehe Es ist ein Fehler aufgetreten. Bitte versuche es erneut. Nicht erreichbar @@ -225,6 +226,8 @@ Getauscht von %s Besuche die Website des Anbieters, um dein Geld zurückzuerhalten Fehler beim Vorgang durch Anbieter + Der Transaktionsbetrag wurde aufgrund von OKX- oder Bridge-Regeln in %1$s auf deine Wallet zurückerstattet. %2$s + Der Betrag wurde in %1$s (%2$s Netzwerk) zurückerstattet. Besuche die Website des Anbieters zur Überprüfung KYC-Überprüfung durch den Anbieter erforderlich Abgebrochen @@ -426,7 +429,7 @@ Jetzt sichern Scannen der Hauptkarte Weiter zu meiner Wallet - Abschließen der Sicherung + Backup abschließen Krypto empfangen Primärkarte scannen Für später überspringen @@ -436,7 +439,7 @@ Erstelle eine Wallet Andere Optionen Deine Schlüssel(private-keys) werden sicher im Inneren der Karte generiert. Es gibt keine Seed-Phrase, d. h. niemand kann sie exportieren oder stehlen. - Schlüssel privat generieren + Schlüssel anonym generieren Deine Karte ist aktiviert und einsatzbereit Erfolgreich! In diesem Fall musst du ganz von vorne anfangen. @@ -454,22 +457,22 @@ leer %d Wörter - Um deine Wallets zu importieren, gib bitte deineSeed-Phrase in das folgende Feld ein + Um deine Wallets zu importieren, gib bitte deine Seed-Phrase in das folgende Feld ein Seed-Phrase generieren Wallet importieren Eine Seed-Phrase ist eine Reihe von Wörtern, mit denen du deine Wallet wiederherstellen kannst. Im Gegensatz zu den von der Karte generierten Schlüsseln sind Seed-Phrasen ungeschützt und können kopiert und gestohlen werden. Die Verwendung dieser Option erfolgt auf eigene Gefahr. Seed-Phrase verwenden Ungültige Seed-Phrase. Bitte überprüfe die Wortreihenfolge. Ungültige Seed-Phrase. Bitte überprüfe die Rechtschreibung. - Altbestand + veralteter Standard Um zu überprüfen, ob du deine Seed-Phrase richtig aufgeschrieben hast, gib bitte das 2., 7. und 11 Wort ein. - Also, lass uns das überprüfen + Eine letzte Prüfung! Um den Sicherungsvorgang zu starten, füge bis zu zwei Sicherungskarten hinzu. Du kannst eine weitere Karte hinzufügen oder den Sicherungsvorgang abschließen Bereite die Sicherungskarte mit der Nummer %s vor. Scanne die primär-Karte, um den Sicherungsvorgang zu starten. Bereite die primäre Karte mit der Nummer %s vor. - Deine Wallet-Karte ist konfiguriert und einsatzbereit. + Deine Tangem-Karte ist konfiguriert und einsatzbereit. Maximale Anzahl an Karten hinzugefügt. Schließe den Sicherungsvorgang ab. Karte aktivieren Sicherungskarte Nr. %d @@ -496,7 +499,7 @@ Gruppe erstellen Nach Guthaben Token organisieren - Gruppierung aufheben + Gruppierung aufh. Wählen aus der Galerie aus Einstellungen Du hast keinen Zugriff auf deine Kamera gewährt @@ -626,7 +629,7 @@ Berühre an beliebiger Stelle für Änderungen Versende %s Du sendest **%1$s** inklusive der Netzwerkgebühr %2$s - Du sendest ** %1$s ** und %2$s + Du sendest **%1$s** und %2$s Senden %s Gesamt %1$s und %2$s werden gesendet @@ -644,9 +647,10 @@ Aktiv Um deine Kryptos zu unstaken, klick hier. Die Anzahl der zu stakenden Krypros muss mindesten %s betragen + nicht gestakte beanspruche + Jährliche prozentuale Rendite + Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. Effektiver Jahreszins - Jährliche prozentuale Rendite - Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. Verfügbar Durchschnittliche Belohnungsquote %s geschätzter Profit @@ -654,7 +658,6 @@ Metriken Mindestanforderungen Keine Belohnungen zu beanspruchen. - Gestaked Belohnungen beanspruchen Eine Möglichkeit, Staking-Belohnungen zu erhalten. Es kann automatisch oder manuell beansprucht werden. Belohnungszeitplan @@ -665,14 +668,28 @@ Der Zeitraum, den du nach der Beantragung der Abhebung von Geldern aus dem Staking warten musst, bevor die Token verfügbar werden. Aufwärmphase Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking. + Stake %s + Migrieren Natives Staking Mit Staking kannst du %1s verdienen. Deine Staking-Belohnungen kommen alle ~%2s Tage. Verdiene Staking-Belohnungen + Die Belohnungen werden sofort nach dem unstaken gestoppt. Der unstakingprozess dauert %s. + Erneut binden + Erneut staken + Belohnungen erneut staken + Widerrufen + Neuwahl Belohnungen + Stake gesperrt Mehr staken - unstaken + gelocktes unlocken + Unstaken Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen + Staking beenden Validator/ Prüfer + Abstimmung + Abstimmung gesperrt + Zurückziehen Bewahre deine Krypto-Assets sicher auf, während die privaten Schlüssel auf deiner Karte bleiben Revolutionäre Hardware-Wallet Bis zu 3 physische Karten pro Wallet @@ -748,8 +765,8 @@ Diese Aktion ist unumkehrbar. Du hast keinen Zugriff mehr auf die alte Wallet. Tippe auf die Doppelkarte mit der Nummer %s und entferne sie erst am Ende des Vorgangs. Verwende %s oder scanne eine Karte, um Zugriff auf deine Wallet zu erhalten. - Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten - Sei der Erste, der von neuen Aktionen erfährt + Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten + Sei der Erste, der von neuen Aktionen erfährt Möchtest du Push-Benachrichtigungen verwenden? Neues Wallet hinzufügen Möchtest du diese Wallet wirklich löschen? @@ -805,9 +822,9 @@ Aktivierungsfehler Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. BNB Beacon Chain wird abgeschaltet - Könnte besser sein + verbesserungswürdig Gefällt mir - OK, ich hab\'s! + OK, habe ich verstanden! Echt toll! Aktualisieren Du befindest sich derzeit im Demo-Modus diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 4326cc2b26..071924cd18 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -87,6 +87,10 @@ Copier l\'adresse Créer Personnalisé + + %d jour + %d jours + Supprimer Désactivé Exécuté @@ -272,8 +276,8 @@ Pour continuer, accordez aux smart contracts de %1s l\'autorisation d\'utiliser votre %2s Donner l\'autorisation Illimité - Commander une carte - Scannez la carte + Commandez + Scannez Touchez, pour modifier le code d\'accès Touchez, pour modifier le mot de passe Pour créer le portefeuille, appuyez sur la carte comme indiqué ci-dessus et ne la retirez pas jusqu\'à la fin de l\'opération @@ -324,6 +328,7 @@ Ajouter au portfolio Mon portfolio Marché + Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem. Sélectionnez un portefeuille Trier par Idées @@ -371,7 +376,7 @@ Voulez-vous quitter le processus d\'activation ? Initialiser Un autre portefeuille a déjà été créé sur la carte que vous essayez d\'ajouter. Si vous avez des fonds dans ce portefeuille, veuillez les retirer, puis réinitialiser cette carte et l\'ajouter comme sauvegarde. - Création d\'une sauvegarde + Sauvegarde en cours En savoir plus sur les seed phrases Empty @@ -384,7 +389,7 @@ Pour importer votre portefeuille, entrez votre seed phrase dans le champ ci-dessous Générer une seed phrase - Importer un portefeuille + Importez Une seed phrase est une série de mots qui vous permet de récupérer votre portefeuille. Contrairement aux clés générées par la carte, les seed phrases ne sont pas protégées et peuvent être copiées et volées. Utilisez cette option à vos propres risques. Utiliser une seed phrase Seed phrase invalide. Veuillez vérifier l\'ordre des mots. @@ -559,6 +564,7 @@ ≈ %1$s (incl. les commissions : %2$s) Sera envoyé %s La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps + %1$s est un actif du réseau Tron. Pour calculer les frais et effectuer une transaction, déposez du Tron (TRX) sur votre compte. Adresse incorrecte %1$s (%2$s) Transaction envoyée @@ -568,9 +574,8 @@ Nom Actif Afin d\'unstaker vos actifs, cliquez ici. + Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking. APR - APY - Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking. Disponible Taux de récompense moyen %s profit estimatif @@ -578,12 +583,11 @@ Métriques Minimum requis Aucune récompense à réclamer - En jeu Réclamation de récompense Un moyen de recevoir des récompenses de staking. Il peut être réclamé automatiquement ou manuellement. Calendrier de récompenses Il s\'agit d\'un calendrier qui détermine le moment où les participants au staking reçoivent leurs récompenses. - Récompenses à réclamer : %s + Récompenses à réclamer: %s Staking %s Période de détachement La période que vous devez attendre après avoir demandé le retrait des fonds du staking avant que les jetons ne soient disponibles. @@ -641,7 +645,7 @@ Masquer le jeton Le Staking vous permet d\'en gagner %1$s et d\'obtenir des récompenses tous les %2$s jours Gagnez jusqu\'à %s récompense de mise par an - %1$s jeton dans %%image%% le réseau %2$s + %1$s jeton dans %%image%% %2$s le réseau Jeton dans le %%image%% %1$s réseau Le jeton %1$s (%2$s) est la principale devise du réseau %3$s et ne peut pas être masqué tant que vous avez d\'autres jetons de ce réseau dans la liste Impossible de masquer %s @@ -669,6 +673,7 @@ Appuyez sur la carte jumelle avec le numéro %s et ne la retirez pas jusqu\'à la fin de l\'opération Utilisez %s ou scannez une carte pour avoir accès à votre portefeuille Restez à jour avec les dernières fonctionnalités et actualités + Soyez le premier informé des nouvelles promotions Souhaitez-vous utiliser les notifications push? Ajouter un nouveau portefeuille Êtes-vous sûr de vouloir supprimer ce portefeuille ? @@ -724,7 +729,7 @@ Erreur d\'activation Selon les développeurs du réseau BNB, le support de la norme BEP-2\nprendra fin en juin 2024. Pour éviter de perdre des actifs avec cette norme, veuillez les convertir à la norme BEP-20. Utilisez notre service de d\'échange pour les transférer sur le réseau BNB Smart Chain. BNB Beacon Chain va s\'arrêter de fonctionner - Pourrait être mieux + Pas terrible J\'aime Ok, je l\'ai! Vraiment cool ! diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index e7caa7829e..b8860bd7d8 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -14,6 +14,7 @@ Mantieni le modifiche Invia Con successo + Avviso Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto Mantenimento della carta diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 2c83a84083..d9c7168c7a 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -89,7 +89,7 @@ 作成 設定 - + %d 日 削除 無効 @@ -108,6 +108,7 @@ 速度と料金 アドレスを取得する プロバイダーへ移動 + トークンへ移動 インポート 後で ロックされています @@ -143,7 +144,7 @@ ステーキング ステーキング 始める - 提出する + 送信 成功 サポート スワップ @@ -223,6 +224,8 @@ %sによる交換 返金を受けるには、プロバイダーのウェブサイトにアクセスしてください。 プロバイダーによる操作が失敗しました。 + OKXまたはブリッジのルールにより、取引金額は%1$sでウォレットに返金されました。%2$s + 金額は %1$s(%2$sネットワーク)で返金されました 確認するには、プロバイダーのウェブサイトにアクセスしてください。 プロバイダーによる本人確認手続きが必要です。 キャンセルされました @@ -335,7 +338,7 @@ 利用可能なネットワーク 私のポートフォリオ マーケット - 選択したネットワークのアドレスを生成するには、Tangemカードをタップする必要があります + 選択したネットワークのアドレスを生成するには、Tangemカードをスキャンする必要があります。 データを読み込めません… クイックアクション 結果 @@ -446,7 +449,7 @@ ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。 シードフレーズを生成する - ウォレットをインポートする + ウォレットをインポート シードフレーズは、ウォレットを復元できる一連の単語です。カードによって生成される秘密鍵とは異なり、シードフレーズは保護されていないため、コピーされて盗まれる可能性があります。このオプションは自己責任で使用してください。 シードフレーズを使用する 無効なシードフレーズです。語順を確認してください。 @@ -613,7 +616,7 @@ 送金中... 変更するには任意の箇所をタップしてください %sを送金する - ** %1$s ** を送金する (ネットワーク手数料%2$sを含む) + **%1$s** を送金する (ネットワーク手数料%2$sを含む) **%1$s** と %2$s を送金しています。 %sを送信しています 合計 @@ -632,17 +635,18 @@ アクティブ 資産のステーキングを解除するには、ここをクリックしてください。 ステーキング金額は %s 以上である必要があります + ステーキング解除分を請求する + APY + ステーキングに参加することで得られる年間収益率。 APR - APY - ステーキングに参加することで得られる年間収益率。 利用可能 平均報酬率 + ステーキングとは? %s 推定利益 市場評価 指標 最低要件 請求できる報酬はありません - ステーキング中 請求中の報酬 ステーキング報酬を受け取る方法。自動または手動で請求できます。 報酬スケジュール @@ -653,14 +657,28 @@ ステーキングから資金の引き出しを要求した後、トークンが利用可能になるまでの待機期間。 ウォームアップ期間 ステーキングへの参加を有効にするために割り当てられた時間。 + %sをステーキングする + 移行 ネイティブステーキング ステーキングにより%1sを獲得できます。ステーキング報酬は ~ %2s日ごとに届きます。 ステーキング報酬を獲得 + ステーキング解除後、報酬の獲得はすぐに停止します。ステーキング解除プロセスには%sかかります。 + 再結束 + 再度ステーキングする + 報酬をステーキングする + 取り消す + 再投票 報酬 + ステーキングはロックされています もっとステーキングする + ステーキング解除はロックされています スタックされていない 資産を請求するために、unstakedを確認してください + ステーキング解除 バリデーター + 投票する + 投票はロックされています + 引き出す カード内に秘密鍵を保管しながら暗号資産を安全に保管します 革新的なハードウェアウォレット 1つのウォレットに最大3枚のカード diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index e83c0613ab..f9f2f33750 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -81,6 +81,7 @@ Перейти на %1$s Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. Отмена + Вывести награду Закрыть Продолжить Копировать @@ -110,6 +111,7 @@ Скорость и комиссия Получить адреса К провайдеру + Перейти в токен Импортировать Позже Заблокирован @@ -142,6 +144,7 @@ Поделиться Подписать Подписать и отправить + Застейкать Стейкинг Начать Отправить @@ -156,6 +159,7 @@ Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно + Завершить стейкинг Да Адрес контракта скопирован! Доступные сети @@ -223,6 +227,8 @@ Обмен через %s Чтобы вернуть ваши деньги, посетите сайт провайдера Операция не выполнена провайдером + Отправленные средства были возвращены в %1$s на ваш кошелек в соответствии с правилами OKX или моста обмена. %2$s + Сумма была возвращена в %1$s (%2$s сети) Посетите сайт провайдера для проверки Провайдер запрашивает прохождение верификации Отменен @@ -277,6 +283,7 @@ Укажите лимит доступа к выбранному токену Количество %s Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов.По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена. + Чтобы продолжить, вам необходимо разрешить смарт контракту StakeKit использовать ваш %s Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s Дать разрешение Безлимитно @@ -327,6 +334,7 @@ Голосовать Выберите кошелек Кошелёк не поддерживает более одной сети + Чтобы создать адреса для выбранных сетей, необходимо отсканировать свой кошелек Tangem. Ссылки Метрики Вам необходимо установить единый код доступа для защиты всех ваших карт @@ -572,20 +580,49 @@ Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя - Сумма для стейкинда должна быть не менее %s - APY - Годовой процентный доход, который вы можете получить от участия в стейкинге. + Активно + Для завершения стейкинга нажмите сюда + Сумма для стейкинга должна быть не менее %s + Забрать средства + APY + Годовой процентный доход, который вы можете получить от участия в стейкинге. + APR Доступно - %s + Средння ставка вознаграждения + %s оценка доходности + Позиция в рынке + Метрики + Минимальное количество + Нет вознаграждений к получению Способ возраграждения - Способ получения вознаграждений за стейкинг. Он может быть автоматическим или ручным. + Способ получения вознаграждений за стейкинг. Он может быть автоматическим, при котором вознаграждение само зачисляется вам на адрес или в ручную, когда вознаграждение нужно вывести, создав транзакцию на её получение. Период возрагражения Это период, определяющий, когда участники стейкинга получат свои вознаграждения. + Вознаграждение для получения: %s Стейкинг %s Период вывода Период, который необходимо подождать после запроса на вывод средств из стейкинга, прежде чем токены станут доступны. Период прогрева - Время, необходимое для начала процесса стейкинга. + Время, необходимое для начала процесса стейкинга и активации процесса начисления наград + Застейкать %s + Переместить + Нативный стейкинг + Стейкинг дает возможность вам получать %1s. Награда будет зачисляться каждый %2s + Получите награду за стейкинг + Награда перестанет начисляться сразу после завершения стейкинга. Процесс завершения длится %s. + Повторный стейкинг + Застейкать вознаграждения + Отозвать + Переголосовать + Вознаграждения + Застейкать еще + Разблокировать + Выведено из стейкинга + Проверьте процесс завершения стейкинга, чтобы вывести свои средства. + Завершение стейкинга + Валидатор + Проголосовать + Вывод Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 4385ef8469..14a8373849 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -114,6 +114,7 @@ Швидкість та комісія Отримати адреси Перейти до провайдера + Перейти до токену Імпортувати Пізніше Заблокований @@ -229,6 +230,8 @@ Обмін через %s Щоб повернути ваші кошти, відвідайте сайт провайдера Операція не виконана провайдером + Сума транзакції була повернута в %1$s на ваш гаманець відповідно до правил OKX або мосту обміну. %2$s + Сума була повернута в %1$s (%2$s мережі) Відвідайте сайт провайдера для перевірки Провайдер вимагає проходження KYC верифікації Скасовано @@ -344,7 +347,7 @@ Доступні мережі Моє портфоліо Маркет - Щоб згенерувати адреси для обраних мереж, потрібно прикласти картку Tangem + Щоб згенерувати адреси для обраних мереж, потрібно відсканувати свою картку Tangem. Не вдалося завантажити дані... Швидкі дії Результат @@ -464,7 +467,7 @@ Щоб імпортувати гаманець, введіть seed-фразу в поле нижче Згенерувати seed-фразу - Імпортувати гаманець + Імпорт гаманця Seed-фраза — це набір слів, який дозволяє відновити ваш гаманець. На відміну від ключів, що генеруються карткою, seed-фраза не захищена і може бути скопійована та викрадена. Використовуйте цю опцію на свій власний ризик. Використовувати seed-фразу Невірна seed-фраза. Будь ласка, перевірте порядок слів. @@ -637,15 +640,15 @@ Надсилання... Торкніться будь-якого поля, щоб змінити його Надіслати %s - Ви надсилаєте ** %1$s **, включно з комісію мережі %2$s - Ви надсилаєте ** %1$s ** і %2$s + Ви надсилаєте **%1$s**, включно з комісію мережі %2$s + Ви надсилаєте **%1$s** і %2$s Надсилання %s Всього %1$s та %2$s буде надіслано ≈ %1$s (вкл. комісію: %2$s ) %s буде надіслано Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час - %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron(TRX) на свій рахунок. + %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron (TRX) на свій рахунок. Недійсна адреса %1$s (%2$s) Трансакцію надіслано @@ -656,9 +659,8 @@ Активний Щоб вивести активи зі стейкінгу, натисніть тут. Сума для стейкінгу має бути не менше %s + Річний відсоток, який ви можете отримати, беручи участь у стейкінгу. APR - APY - Річний відсоток, який ви можете отримати, беручи участь у стейкінгу. Доступно Середня ставка винагороди ~ прибуток за %s @@ -666,7 +668,6 @@ Метрики Мінімальні вимоги Немає винагород, щоб отримати - В стейкінгу Отримати винагороду Спосіб отримання винагороди за стейкінг. Його можна отримати автоматично або вручну. Розклад винагород @@ -677,6 +678,7 @@ Період, який ви повинні чекати після запиту на виведення коштів зі стейкінгу, перш ніж токени стануть доступними. Період блокування Відведений час для активації участі в стейкінгу. + Стейкінг %s Нативний стейкінг Стейкінг дозволяє заробляти %1s. Ваші винагороди за стейкінг надходять кожні ~%2s днів. Отримуйте винагороду за стейкінг @@ -817,7 +819,7 @@ Помилка активації За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. Відключення мережі BNB Beacon Chain - Могло б бути краще + Можна краще Вподобати Зрозуміло! Дуже круто! 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 ae1e771935..b198f3eee7 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -9,6 +9,9 @@ 原因:%s 無法發送交易 此卡不支持%1$s網路上的代幣因為韌體限制 + 感謝您的反饋。我們會盡快回复 + 你的建議已送出 + 請嘗試完全按照動畫中顯示的方式點擊卡片或請求支持 有困難在掃描卡上嗎? 此卡不適用於此app 轉到設置以在 Tangem App 中啟用生物識別身份驗證 @@ -53,6 +56,7 @@ 創造 刪除 禁用 + 斷開連接 完成 允許 啟用 @@ -65,6 +69,7 @@ 主卡片 拒絕 重新命名 + 重試 保存設置 搜索 搜尋代幣 @@ -83,6 +88,7 @@ 交易 我了解 無法觸達 + 警告 已複製代幣地址 支持的網路 @@ -115,6 +121,7 @@ App Currency 發行人 簽署 + 如果您忘記密碼,您將無法使用您的資金。無法恢復代碼 更多 檢查您的網路連接或切換到其他網絡 服務條款 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index aaba92efbb..b277ef22a8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -4,21 +4,15 @@ Add custom token Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. - Thank you for your feedback - Sent successfully How to scan Request support Try again This feature is disabled in Demo mode - Failed to send the email Reason: %s Can\'t send a transaction The selected does not support the %1$s network To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. Tokens in %1$s network are not supported by this card due to firmware limitation. - Thank you for your feedback. We will respond as soon as possible - Your suggestions were sent - Please try to tap the card exactly as shown in the animation or read our simple guide, or request support. If the problem persists, please request support. Are you having difficulty scanning your card? This card is not designed to work with this app Default Fee @@ -34,12 +28,8 @@ Dark Light System default - If system is selected, the app will auto-adjust based on your device\'s system settings - System Theme App settings - Go to settings to enable biometric authentication in the Tangem app - Enable biometric authentication To hide or show your balances, simply flip your device screen down, or switch it off in Settings Don\'t show again Got it @@ -48,7 +38,6 @@ Please try again in 30 seconds or scan the card Too many attempts You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. - Touch ID is used to save your cards in the app Start backup process With your bank card or bank account @@ -57,7 +46,6 @@ Disable this option if you don\'t want this card to be used to reset access codes on other cards in this wallet. Please note that this will also prevent you from resetting the access code on this card. Allows you to use this card to reset access code on other cards in this wallet - Disable the ability to reset the access code on this card or other cards in this wallet Access code recovery Reset Are you sure you want to do this? @@ -86,16 +74,13 @@ Approval Approve Attention - Back Balance: %s Balance biometric authentication biometrics Buy Go to %1$s - Settings You have not given access to your camera, please adjust your privacy settings - Camera access denied Cancel Claim rewards Close @@ -110,7 +95,6 @@ Delete Disabled - Disconnect Done Enable Enabled @@ -129,7 +113,6 @@ Go to token Import Later - Learn & Earn Locked Main network Network fee @@ -142,14 +125,12 @@ Primary Card Passphrase Paste - Push %1$s-%2$s Read more Receive Reject Reload Rename - Retry Save Save changes Search @@ -178,15 +159,12 @@ There was an error. Please try again. Unreachable Unstake - Warning Yes Contract address copied! Available networks Add token Contract address - Please fill in all the fields Contract address is invalid - Derivation path is invalid Please select the network Decimal must be a valid integer, up to %li Custom derivation @@ -216,7 +194,6 @@ You will have to submit the correct access code before scanning the card Long Tap This mechanism protects against proximity attacks on a card. It will enforce a delay between reception and execution of a command. - Long Tap Passcode Before executing any command entailing a change of the card state, you will have to enter the passcode. Referral program @@ -225,12 +202,10 @@ Card ID Contact support Link More Cards - You can synchronize up to three cards into one wallet. It can only be done once. App Currency Flip-to-Hide Balances Issuer Signed - If you forget the code you will lose access to your funds. Code recovery is not possible. Send feedback Details Check your internet connection or switch to a different network @@ -240,7 +215,6 @@ You haven\'t added any tokens yet. Add tokens via Market to swap Cannot be swapped for %s Provided by - Provided by %s Status Tangem offers token swaps via 3rd-party providers according to each provider\'s terms Choose provider @@ -304,7 +278,6 @@ Feedback Tangem feedback Can\'t send a transaction - Can\'t push a transaction Current transaction The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Specify the approve limit for the selected token @@ -319,10 +292,8 @@ To change the access code tap the card as shown above and do not remove until the end of the operation To change the passcode tap the card as shown above and do not remove until the end of the operation To create the wallet tap the card as shown above and do not remove until the end of the operation - To reset to factory settings tap the card as shown above and do not remove until the end of the operation Tap the card #%s of the wallet Tap to scan - To sign tap the card as shown above and do not remove until the end of the operation Tap to sign Tap the card You have updated biometrics, scan your card to enter @@ -334,8 +305,6 @@ Mana limit The Koinos network requires Mana for network fees. Your have %1$s/%2$s Mana Mana level - Please, set up an account to send email - No Mail accounts To begin tracking your crypto assets and transactions, add tokens Manage tokens To access all the networks you need to scan the card @@ -358,11 +327,6 @@ %1$d of %2$d wallet %1$d of %2$d wallets - %d of %#@total_wallets@ - - %d wallet - %d wallets - Remove e.g. BTC I trust, hodl I must Your portfolio has been updated @@ -454,7 +418,6 @@ Activation error Add tokens You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process? - iPhone 7/7+ is not able to create a backup for Tangem Wallet due to some system limitations. Please use another phone to perform this operation. All other functions work stably. The backup process is partly complete. You can\'t exit it now. The passphrase is an advanced security feature that crypto wallets use. It adds an extra word or phrase of your own choosing to your already existing recovery phrase to unlock a brand-new set of addresses. Add a backup card @@ -479,7 +442,6 @@ Do you want to exit the activation process? Getting started Another wallet has already been created on the card you\'re trying to add. If you have funds in this wallet, please withdraw it and then reset this card and add it as a backup. - Save your wallet Creating a backup Read more about seed phrase @@ -499,7 +461,6 @@ Invalid seed phrase. Please check the word order. Invalid seed phrase. Please check your spelling. Legacy - We do not recommend storing the seed phrase as a screenshot due to the high risk of loss or hacking To check whether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words So, let’s check To start the backup process add up to two backup cards. @@ -535,9 +496,6 @@ By balance Organize tokens Ungroup - Additional fee - Previous fee - Previous transaction total including fee Select from the gallery Settings You have not given access to your camera @@ -548,7 +506,6 @@ Participate Failed to load the information about the referral program. Please try again later. Failed to load the information about the referral program. Error code: %s. Please try again later. - Your participation request could not be processed. Error code: %s. Please try again later. If the problem persists — feel free to contact our support. Upcoming payments Your friends bought Less @@ -584,15 +541,11 @@ Russian bank cards are not currently accepted Log into the app and check your balance without scanning the card Access the app - Allow to use %s Allow to use biometrics - %s will be requested instead of the access code for interactions with your wallet Biometrics will be requested instead of the access code for interactions with your wallet Access code - Don\'t allow It looks like you have biometric authentication disabled, it is necessary to save wallets Enable biometric authorization - Would you like to use %s? Would you like to use biometrics? Note that making a transaction with your funds will still require your card Scan Card @@ -615,7 +568,6 @@ %1$s, %2$s Address Destination Tag - Are you sure you want to close the send screen? Enter address Address is the same as wallet address Invalid Tag. It won\'t be added to the transaction. @@ -628,7 +580,6 @@ Priority Check your network connection Network fee info unreachable - From **%s** From Gas limit This is the maximum amount of gas that will be spent to complete a transaction or contract. A gas limit prevents unexpected or unlimited charges when executing a transaction. @@ -693,17 +644,17 @@ To unstake your assets, click here. The amount to stake must be at least %s Claim unstaked + Annual percentage rate + The annual percentage return you can earn from participating in staking. APR - APY - The annual percentage return you can earn from participating in staking. Available Average Reward Rate + What is Staking? %s est. profit Market rating Metrics Minimum Requirement No rewards to claim - On stake Reward claiming A way to receive staking rewards. It can be claimed automatically or manually. Reward schedule @@ -719,6 +670,7 @@ Native staking Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days. Earn staking rewards + Rewards stop accruing immediately after you unstake. The unstaking process takes %s. Rebond Restake Restake rewards @@ -743,8 +695,6 @@ Thousands of Currencies Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. The Wallet for Everyone - Take three lessons, get a discount on your Tangem Wallet, and receive 1INCH tokens to your wallet - Learn Meet Tangem Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services Web 3.0 Compatible @@ -760,7 +710,6 @@ Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Give Permission - Permit and Swap In progress Swap You receive @@ -778,7 +727,6 @@ Sending funds will be available once the pending transaction(s) in network %s is complete Selling %s is not available at the moment. Please check our updates. Staking %s is not available at the moment. Please check our updates. - Choose address Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. @@ -826,13 +774,6 @@ Rename Wallet Unlock all Unlock all with %s - Close network fee settings - Nothing to paste from clipboard - Open card details - Open network fee settings - Scan QR code to open new WalletConnect session - Paste address from clipboard - Scan, QR, code with address Blockchain is unreachable. Try later Scan the card Requesting to sign a message.\n\n%s @@ -911,8 +852,6 @@ This token must be associated with your Hedera account before you can receive it Associate your token Not enough %s. Top up your Hedera account to associate this token - iPhone 7/7+ cannot sign transactions on this network. To complete this operation, please use a different phone. - Transaction signing unavailable Only %s signatures are left on this card. You must withdraw all of your funds. Low signature count Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. @@ -928,10 +867,6 @@ Missing backup This card has been previously used for transactions. If received from an untrusted source, consider withdrawing all funds. If it\'s your card, no action is required. Card has already signed transactions - Funds in Tangem cards issued before September 2019 cannot be retrieved on iPhones due to iOS restrictions. Please use an Android phone for retrieval. Cards issued after September 2019 work correctly on both OS. - iOS restriction for older cards - Some iPhone 7/7+ models may have NFC issues during certain operations. - Device incompatibility detected Your review keeps us motivated to make Tangem Wallet even better Enjoying Tangem? You must associate your token before receiving tokens @@ -943,10 +878,6 @@ Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. Some networks currently are unreachable. Please try again later. Some networks are unreachable - System update required - Support for your version of the operating system will end on %s. To receive future app updates you must update it to the latest version. - Tangem recommends installing the latest iOS update for stable and safe operation - System update available This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. For testing purposes only Discard diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotification.kt new file mode 100644 index 0000000000..e1b4cc4b24 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotification.kt @@ -0,0 +1,125 @@ +package com.tangem.core.ui.components.notifications + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.colorspace.ColorSpaces +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Currency notification component from Design system. + * + * @param config component config + * @param modifier modifier + * @param containerColor container color + * + * @see Figma component + */ +@Composable +fun CurrencyNotification( + config: CurrencyNotificationConfig, + modifier: Modifier = Modifier, + containerColor: Color? = null, +) { + NotificationBaseContainer( + buttonsState = config.buttonsState, + onClick = null, + onCloseClick = null, + modifier = modifier, + containerColor = containerColor, + ) { + MainContent( + tokenIconState = config.tokenIconState, + title = config.title, + subtitle = config.subtitle, + ) + } +} + +@Composable +private fun MainContent( + tokenIconState: CurrencyIconState, + title: TextReference, + subtitle: CurrencyNotificationConfig.AnnotatedSubtitle, +) { + Row { + CurrencyIcon( + state = tokenIconState, + modifier = Modifier.align(alignment = Alignment.CenterVertically), + ) + + SpacerW(width = TangemTheme.dimens.spacing6) + + TextsBlock(title = title, subtitle = subtitle) + } +} + +@Composable +private fun TextsBlock(title: TextReference, subtitle: CurrencyNotificationConfig.AnnotatedSubtitle) { + Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2)) { + Text( + text = title.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + ) + + val subtitleValue = subtitle.valueProvider() + ClickableText( + text = subtitleValue, + onClick = { subtitle.onClick(subtitleValue, it) }, + style = TangemTheme.typography.caption2, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_Notification() { + TangemThemePreview { + CurrencyNotification( + config = CurrencyNotificationConfig( + title = resourceReference( + R.string.express_exchange_notification_refund_title, + wrappedList("USDT", "Polygon"), + ), + subtitle = CurrencyNotificationConfig.AnnotatedSubtitle( + valueProvider = { + buildAnnotatedString { + append("Your transaction amount was refunded in USDT to your wallet due to OKX") + } + }, + onClick = { _, _ -> }, + ), + tokenIconState = CurrencyIconState.TokenIcon( + url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/usd-coin.png", + topBadgeIconResId = R.drawable.ic_polygon_22, + isGrayscale = false, + showCustomBadge = false, + fallbackTint = Color(1.0f, 1.0f, 1.0f, 1.0f, ColorSpaces.Srgb), + fallbackBackground = Color(0.23529412f, 0.28627452f, 0.6117647f, 1.0f, ColorSpaces.Srgb), + ), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = stringReference("Go to token"), + onClick = {}, + ), + ), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotificationConfig.kt new file mode 100644 index 0000000000..74962546e7 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/CurrencyNotificationConfig.kt @@ -0,0 +1,35 @@ +package com.tangem.core.ui.components.notifications + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.AnnotatedString +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.TextReference + +/** + * Currency notification component state + * + * @property title title + * @property subtitle subtitle + * @property buttonsState buttons state + * @property tokenIconState token icon state + * +[REDACTED_AUTHOR] + */ +data class CurrencyNotificationConfig( + val title: TextReference, + val subtitle: AnnotatedSubtitle, + val tokenIconState: CurrencyIconState, + val buttonsState: NotificationConfig.ButtonsState, +) { + + /** + * Subtitle as [AnnotatedString] + * + * @property valueProvider composable function that provides [AnnotatedString] + * @property onClick lambda be invoked when text in specified position is clicked + */ + data class AnnotatedSubtitle( + val valueProvider: @Composable () -> AnnotatedString, + val onClick: (value: AnnotatedString, position: Int) -> Unit, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index c5b3d10a9b..f25c4014cf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -30,17 +30,19 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState /** * Notification component from Design system. * Use this for Notification with title, subtitle, clickable or not. * - * @param config component config - * @param modifier modifier - * @param iconTint icon tint + * @param config component config + * @param modifier modifier + * @param containerColor container color + * @param iconTint icon tint + * @param isEnabled flag that defines if component is clickable * * @see Figma component @@ -53,44 +55,33 @@ fun Notification( iconTint: Color? = null, isEnabled: Boolean = true, ) { - BaseContainer( + NotificationBaseContainer( buttonsState = config.buttonsState, onClick = config.onClick, + onCloseClick = config.onCloseClick, modifier = modifier, containerColor = containerColor, isEnabled = isEnabled, ) { - Column( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), - ) { - MainContent( - iconResId = config.iconResId, - iconTint = iconTint, - title = config.title, - subtitle = config.subtitle, - isClickableComponent = isEnabled && config.onClick != null, - ) - - Buttons(state = config.buttonsState, isEnabled = isEnabled) - } - - CloseableIconButton( - onClick = config.onCloseClick, - modifier = Modifier.align(alignment = Alignment.TopEnd), - isEnabled = isEnabled, + MainContent( + iconResId = config.iconResId, + iconTint = iconTint, + title = config.title, + subtitle = config.subtitle, + isClickableComponent = isEnabled && config.onClick != null, ) } } @Composable -private fun BaseContainer( +internal fun NotificationBaseContainer( buttonsState: NotificationConfig.ButtonsState?, onClick: (() -> Unit)?, + onCloseClick: (() -> Unit)?, modifier: Modifier = Modifier, isEnabled: Boolean = true, containerColor: Color? = null, - content: @Composable BoxScope.() -> Unit, + content: @Composable ColumnScope.() -> Unit, ) { val tempContainerColor by rememberUpdatedState( newValue = if (buttonsState != null || onClick != null) { @@ -109,7 +100,22 @@ private fun BaseContainer( shape = TangemTheme.shapes.roundedCornersXMedium, color = containerColor ?: tempContainerColor, ) { - Box(content = content) + Box { + Column( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), + ) { + content() + + Buttons(state = buttonsState, isEnabled = isEnabled) + } + + CloseableIconButton( + onClick = onCloseClick, + modifier = Modifier.align(alignment = Alignment.TopEnd), + isEnabled = isEnabled, + ) + } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index 6b4cc1bd3d..220b5c82ca 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -82,7 +82,9 @@ private fun LazyListScope.contentItems( when (item) { is TxHistoryState.TxHistoryItemState.GroupTitle -> item.itemKey is TxHistoryState.TxHistoryItemState.Title -> item.onExploreClick.hashCode() - is TxHistoryState.TxHistoryItemState.Transaction -> item.state.txHash + is TxHistoryState.TxHistoryItemState.Transaction -> + item.state.txHash + + ((item.state as? TransactionState.Content)?.hashCode() ?: "") } }, contentType = txHistoryItems.itemContentType { it::class.java }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 778d4f08d8..e5d94f85b7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -37,7 +37,7 @@ object BigDecimalFormatter { maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) minimumFractionDigits = 2 isGroupingUsed = true - roundingMode = RoundingMode.DOWN + roundingMode = RoundingMode.HALF_UP } return formatter.format(cryptoAmount).let { diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt index 575485aae9..1b21fff1ef 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.MutableState import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState -import com.google.accompanist.permissions.shouldShowRationale /** * Returns push permission requester. @@ -16,22 +15,13 @@ import com.google.accompanist.permissions.shouldShowRationale @OptIn(ExperimentalPermissionsApi::class) @Composable fun requestPushPermission( - isFirstTimeAsking: Boolean, pushPermission: String?, isClicked: MutableState, onAllow: () -> Unit, onDeny: () -> Unit, - onOpenSettings: () -> Unit, ): () -> Unit { val permissionState = pushPermission?.let { permission -> - val tempPermissionState = rememberPermissionState(permission = permission) - rememberPermissionState(permission = permission) { - when { - it -> onAllow() - !tempPermissionState.status.shouldShowRationale && !isFirstTimeAsking -> onOpenSettings() - else -> onDeny() - } - } + rememberPermissionState(permission = permission) } // Check if user granted permission and close bottom sheet @@ -45,7 +35,7 @@ fun requestPushPermission( } return if (permissionState == null) { - onOpenSettings + {} } else { permissionState::launchPermissionRequest } diff --git a/core/ui/src/main/res/drawable/ic_more_cards_24.xml b/core/ui/src/main/res/drawable/ic_more_cards_24.xml new file mode 100644 index 0000000000..1fd80fa554 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_more_cards_24.xml @@ -0,0 +1,11 @@ + + + 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 d0250fd7c3..5241c8562b 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 @@ -80,24 +80,10 @@ internal class DefaultCardRepository( 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() @@ -127,16 +113,5 @@ internal class DefaultCardRepository( .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 - } - } - private fun createDefaultUsedCardInfo(cardId: String) = UsedCardInfo(cardId = cardId) } \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index e48f0e6933..94a184fb73 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -33,19 +33,17 @@ internal class DefaultPromoRepository( } override suspend fun getOkxPromoBanner(): PromoBanner? { - // TODO disabled for 5.12, enable for 5.12.1 - return null - // return runCatching(dispatchers.io) { - // promoResponseConverter.convert( - // tangemApi.getPromotionInfo(OKX) - // .getOrThrow(), - // ) - // }.getOrNull() + return runCatching(dispatchers.io) { + promoResponseConverter.convert( + tangemApi.getPromotionInfo(OKX) + .getOrThrow(), + ) + }.getOrNull() } private companion object { private const val CHANGELLY_NAME = "changelly" private const val TRAVALA = "travala" - // private const val OKX = "okx" + private const val OKX = "okx" } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt index 6497ea59b4..e2528964fe 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt @@ -1,11 +1,6 @@ package com.tangem.data.settings -import android.os.SystemClock import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.PreferencesKeys.getIsFirstTimeAskingPermission -import com.tangem.datasource.local.preferences.PreferencesKeys.getPermissionDaysCount -import com.tangem.datasource.local.preferences.PreferencesKeys.getPermissionLaunchCount import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowInitialPermissionScreen import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowPermission import com.tangem.datasource.local.preferences.utils.getSyncOrDefault @@ -30,47 +25,11 @@ internal class DefaultPermissionRepository( ) } - override suspend fun isFirstTimeAskingPermission(permission: String): Boolean = - appPreferencesStore.getSyncOrDefault( - key = getIsFirstTimeAskingPermission(permission), - default = true, - ) - - override suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean) { - appPreferencesStore.store( - key = getIsFirstTimeAskingPermission(permission), - value = value, - ) - } - override suspend fun shouldAskPermission(permission: String): Boolean { - val shouldAskPermission = appPreferencesStore.getSyncOrDefault(getShouldShowPermission(permission), true) - val delayedLaunches = appPreferencesStore.getSyncOrDefault(getPermissionLaunchCount(permission), 0) - val delayedDays = appPreferencesStore.getSyncOrDefault(getPermissionDaysCount(permission), 0) - val currentLaunchCounter = appPreferencesStore.getSyncOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0) - - val nowMillis = SystemClock.elapsedRealtime() - val isDaysDelayed = delayedDays < nowMillis - val isLaunchesDelayed = delayedLaunches < currentLaunchCounter - return shouldAskPermission && isDaysDelayed && isLaunchesDelayed + return appPreferencesStore.getSyncOrDefault(getShouldShowPermission(permission), true) } override suspend fun neverAskPermission(permission: String) { appPreferencesStore.store(key = getShouldShowPermission(permission), value = false) } - - override suspend fun delayPermissionAsking(permission: String) { - appPreferencesStore.editData { - val appLaunchCounter = it.getOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0) - val nowMillis = SystemClock.elapsedRealtime() - - it[getPermissionLaunchCount(permission)] = appLaunchCounter + DELAY_LAUNCH_COUNT - it[getPermissionDaysCount(permission)] = nowMillis + DELAY_DAYS_COUNT - } - } - - private companion object { - const val DELAY_LAUNCH_COUNT = 5 - const val DELAY_DAYS_COUNT = 3L * 24 * 3600 * 1000 // 3 days in millis - } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index dcfd66e7a7..b24c5226bc 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency -@Suppress("LargeClass", "LongParameterList") +@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, private val tangemExpressApi: TangemExpressApi, @@ -53,6 +53,7 @@ internal class DefaultCurrenciesRepository( private val demoConfig = DemoConfig() private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory() + private val cryptoCurrencyFactory = CryptoCurrencyFactory() private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig) private val userTokensResponseFactory = UserTokensResponseFactory() private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() @@ -61,18 +62,6 @@ internal class DefaultCurrenciesRepository( private val isMultiCurrencyWalletCurrenciesFetching = MutableStateFlow( value = emptyMap(), ) - private val parallelTransactionsEnabledBlockchains = setOf( - Blockchain.Ethereum, - Blockchain.EthereumTestnet, - Blockchain.Polygon, - Blockchain.PolygonTestnet, - Blockchain.Arbitrum, - Blockchain.ArbitrumTestnet, - Blockchain.Binance, - Blockchain.BinanceTestnet, - Blockchain.Tron, - Blockchain.TronTestnet, - ) override suspend fun saveTokens( userWalletId: UserWalletId, @@ -137,7 +126,7 @@ internal class DefaultCurrenciesRepository( return newTokens .filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins .mapNotNull { - CryptoCurrencyFactory().createCoin( + cryptoCurrencyFactory.createCoin( blockchain = getBlockchain(networkId = it.network.id), extraDerivationPath = it.network.derivationPath.value, derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider, @@ -391,7 +380,8 @@ internal class DefaultCurrenciesRepository( val outgoingTransactions = cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing } outgoingTransactions.isNotEmpty() } - parallelTransactionsEnabledBlockchains.contains(blockchain) -> false + blockchain.isEvm() -> false + blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet -> false else -> coinStatus?.value?.hasCurrentNetworkTransactions == true } } @@ -426,12 +416,44 @@ internal class DefaultCurrenciesRepository( } override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token { - return CryptoCurrencyFactory().createToken( + return cryptoCurrencyFactory.createToken( cryptoCurrency = cryptoCurrency, network = network, ) } + override suspend fun createTokenCurrency( + userWalletId: UserWalletId, + contractAddress: String, + networkId: String, + ): CryptoCurrency.Token { + val userWallet = getUserWallet(userWalletId) + val token = withContext(dispatchers.io) { + val foundToken = tangemTechApi.getCoins( + contractAddress = contractAddress, + networkId = networkId, + ) + .getOrThrow() + .coins + .firstOrNull() + ?: error("Token not found") + val network = foundToken.networks.firstOrNull { it.networkId == networkId } ?: error("Network not found") + CryptoCurrencyFactory.Token( + symbol = foundToken.symbol, + name = foundToken.name, + contractAddress = contractAddress, + decimals = network.decimalCount?.toInt() ?: error("Decimals not found"), + id = foundToken.id, + ) + } + return cryptoCurrencyFactory.createToken( + token = token, + networkId = networkId, + extraDerivationPath = null, + derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, + ) ?: error("Unable to create token") + } + private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { return userTokensStore.get(userWallet.walletId).map { storedTokens -> responseCurrenciesFactory.createCurrencies( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt index 55b2b0b161..b1e215ed19 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt @@ -99,7 +99,7 @@ class CryptoCurrencyFactory { decimals = cryptoCurrency.decimals, id = cryptoCurrency.id.rawCurrencyId, ) - val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.id.value) ?: Blockchain.Unknown + val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown val id = getTokenId(network, sdkToken) return CryptoCurrency.Token( id = id, diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index caffe5aca9..56922c5bf0 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -24,7 +24,6 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber -import java.math.BigDecimal import java.math.BigInteger import com.tangem.blockchain.blockchains.tron.TransactionType as SdkTransactionType @@ -41,7 +40,6 @@ internal class DefaultTransactionRepository( destination: String, userWalletId: UserWalletId, network: Network, - isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, ): TransactionData.Uncompiled? = withContext(coroutineDispatcherProvider.io) { @@ -58,7 +56,6 @@ internal class DefaultTransactionRepository( memo = memo, destination = destination, network = network, - isSwap = isSwap, txExtras = txExtras, hash = hash, ) @@ -91,7 +88,6 @@ internal class DefaultTransactionRepository( memo = memo, destination = destination, network = network, - isSwap = isSwap, txExtras = txExtras, hash = hash, ) @@ -158,23 +154,15 @@ internal class DefaultTransactionRepository( memo: String?, destination: String, network: Network, - isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, ): TransactionData.Uncompiled { - // TODO: refactor workaround to use general mechanism in bsdk for build tx for DEX - val txAmount = if (isSwap) { - createAmountForSwap(amount) - } else { - amount - } - if (txExtras != null && memo != null) { // throw error for now to avoid programmers errors when use extras error("Both txExtras and memo provided, use only one of them") } val extras = txExtras ?: getMemoExtras(network.id.value, memo) - return createTransaction(txAmount, fee, destination).copy( + return createTransaction(amount, fee, destination).copy( hash = hash, extras = extras, ) @@ -203,19 +191,4 @@ internal class DefaultTransactionRepository( else -> null } } - - private fun createAmountForSwap(amount: Amount): Amount { - return when (amount.type) { - is AmountType.Coin -> amount - else -> { - // 1. when creates swap amount for NonNativeToken, amount should be ZERO - // 2. Amount has .Coin type, as workaround to use destinationAddress in bsdk, not contractAddress - Amount( - currencySymbol = amount.currencySymbol, - value = BigDecimal.ZERO, - decimals = amount.decimals, - ) - } - } - } } \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt index d534e238ec..87d2138b87 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt @@ -85,6 +85,7 @@ internal class TxHistoryPagingSource( currency = sourceParams.currency, ) .filterUnconfirmedTransaction() + .sortedByDescending { it.timestampInMillis } .filterIfTxAlreadyAdded(apiItems = items) return if (recentItems.isEmpty()) { 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 68e476443e..5e01929fa1 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 @@ -26,10 +26,5 @@ interface CardRepository { @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 diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index ac9b4fa892..2536d78ea9 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -1,5 +1,6 @@ package com.tangem.domain.exchange +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency /** @@ -7,7 +8,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency */ interface RampStateManager { - fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean + fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt deleted file mode 100644 index bf300cb779..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.domain.settings - -import arrow.core.Either -import com.tangem.domain.settings.repositories.PermissionRepository - -class DelayPermissionRequestUseCase( - private val repository: PermissionRepository, -) { - - suspend operator fun invoke(permission: String): Either = Either.catch { - repository.delayPermissionAsking(permission) - } -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt deleted file mode 100644 index 000bfd7ed1..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.domain.settings - -import arrow.core.Either -import com.tangem.domain.settings.repositories.PermissionRepository - -class IsFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) { - - suspend operator fun invoke(permission: String): Either = Either.catch { - repository.isFirstTimeAskingPermission(permission) - } -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt deleted file mode 100644 index fa5d057e77..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.domain.settings - -import arrow.core.Either -import com.tangem.domain.settings.repositories.PermissionRepository - -class SetFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) { - - suspend operator fun invoke(permission: String): Either = Either.catch { - repository.setFirstTimeAskingPermission(permission, false) - } -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt index 721bfe90be..21b476b233 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt @@ -13,18 +13,6 @@ interface PermissionRepository { */ suspend fun neverInitiallyShowPermissionScreen(permission: String) - /** - * Indicates which time [permission] was asked via platform dialog. - * NOTE: Use this method to indicate either reroute to settings or display platform dialog. - */ - suspend fun isFirstTimeAskingPermission(permission: String): Boolean - - /** - * Sets value indicating that [permission] was asked via platform dialog. - * NOTE: Use this method to indicate either reroute to settings or display platform dialog. - */ - suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean) - /** * Is clear to ask [permission]. * User could already granted or permanently denied permission @@ -36,9 +24,4 @@ interface PermissionRepository { * Permanently deny [permission] and never request again */ suspend fun neverAskPermission(permission: String) - - /** - * Delay next [permission] request for some time or active sessions - */ - suspend fun delayPermissionAsking(permission: String) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index e8c6f0b7a8..01223c61a3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -79,13 +79,35 @@ class AddCryptoCurrenciesUseCase( .toNonEmptyListOrNull() ?: return@either - catch({ currenciesRepository.addCurrencies(userWalletId, currenciesToAdd) }) { - raise(it) - } - + addCurrencies(userWalletId, currenciesToAdd) refreshUpdatedNetworks(userWalletId, currenciesToAdd, existingCurrencies) } + suspend operator fun invoke( + userWalletId: UserWalletId, + contractAddress: String, + networkId: String, + ): Either = either { + val existingCurrencies = + catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) { + raise(it) + } + val foundToken = existingCurrencies + .filterIsInstance() + .firstOrNull { + it.network.backendId == networkId && + !it.isCustom && + it.contractAddress.equals(contractAddress, true) + } + if (foundToken != null) { + return@either foundToken + } + val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId) + addCurrencies(userWalletId, listOf(tokenToAdd)) + refreshUpdatedNetworks(userWalletId, listOf(tokenToAdd), existingCurrencies) + tokenToAdd + } + /** * Refreshes the network statuses for tokens that have corresponding coins in the * [existingCurrencies] list. @@ -117,6 +139,33 @@ class AddCryptoCurrenciesUseCase( } } + private suspend fun Raise.createTokenCurrency( + userWalletId: UserWalletId, + contractAddress: String, + networkId: String, + ): CryptoCurrency.Token { + return catch( + block = { + currenciesRepository.createTokenCurrency( + userWalletId = userWalletId, + contractAddress = contractAddress, + networkId = networkId, + ) + }, + catch = { + raise(it) + }, + ) + } + + private suspend fun Raise.addCurrencies(userWalletId: UserWalletId, tokens: List) { + catch( + { currenciesRepository.addCurrencies(userWalletId, tokens) }, + ) { + raise(it) + } + } + /** * Determines if the [existingCurrencies] list contains a coin that corresponds * to the given [token]. diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 16083adcbb..c83af879ba 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -106,7 +106,7 @@ class GetCryptoCurrencyActionsUseCase( return listOf(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) } if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable) { - return getActionsForUnreachableCurrency(cryptoCurrencyStatus, needAssociateAsset) + return getActionsForUnreachableCurrency(userWallet, cryptoCurrencyStatus, needAssociateAsset) } val activeList = mutableListOf() @@ -168,7 +168,7 @@ class GetCryptoCurrencyActionsUseCase( } // buy - if (rampManager.availableForBuy(cryptoCurrency)) { + if (rampManager.availableForBuy(userWallet.scanResponse, cryptoCurrency)) { activeList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) } else { disabledList.add( @@ -222,6 +222,7 @@ class GetCryptoCurrencyActionsUseCase( } private fun getActionsForUnreachableCurrency( + userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, needAssociateAsset: Boolean, ): List { @@ -230,7 +231,7 @@ class GetCryptoCurrencyActionsUseCase( if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { actionsList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) } - if (rampManager.availableForBuy(cryptoCurrencyStatus.currency)) { + if (rampManager.availableForBuy(userWallet.scanResponse, cryptoCurrencyStatus.currency)) { actionsList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) } else { actionsList.add( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 782c16153e..4d8d4f0522 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.Flow /** * Repository for everything related to the tokens of user wallet * */ +@Suppress("TooManyFunctions") interface CurrenciesRepository { /** @@ -213,4 +214,13 @@ interface CurrenciesRepository { * Creates token [cryptoCurrency] based on current token and [network] it`s will be added */ fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token + + /** + * Creates token [cryptoCurrency] based on [contractAddress] and [networkId] it`s will be added + */ + suspend fun createTokenCurrency( + userWalletId: UserWalletId, + contractAddress: String, + networkId: String, + ): CryptoCurrency.Token } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index e81b9e83d1..c1c3aa5e5a 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -136,4 +136,12 @@ internal class MockCurrenciesRepository( override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token { return cryptoCurrency } + + override suspend fun createTokenCurrency( + userWalletId: UserWalletId, + contractAddress: String, + networkId: String, + ): CryptoCurrency.Token { + error("not implemented") + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 2228ec19e5..3fb7cbf0b5 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -18,7 +18,6 @@ interface TransactionRepository { destination: String, userWalletId: UserWalletId, network: Network, - isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, ): TransactionData.Uncompiled? diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt index 056dfc0f0c..ab5172eaf8 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt @@ -24,7 +24,6 @@ class CreateTransactionUseCase( userWalletId: UserWalletId, network: Network, txExtras: TransactionExtras? = null, - isSwap: Boolean = false, hash: String? = null, ) = Either.catch { requireNotNull( @@ -35,7 +34,6 @@ class CreateTransactionUseCase( destination = destination, userWalletId = userWalletId, network = network, - isSwap = isSwap, txExtras = txExtras, hash = hash, ), diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index faccbea7e4..96b5db01fa 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -14,6 +14,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.DemoTransactionSender import com.tangem.domain.tokens.model.Network @@ -37,7 +38,10 @@ class SendTransactionUseCase( userWallet: UserWallet, network: Network, ): Either { - val signer = cardSdkConfigRepository.getCommonSigner(cardId = null) + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + + val signer = cardSdkConfigRepository.getCommonSigner(cardId = card.cardId.takeIf { isCardNotBackedUp }) val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal() if (userWallet.scanResponse.card.isStart2Coin) { @@ -105,22 +109,7 @@ class SendTransactionUseCase( } val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError() return when (error) { - is BlockchainSdkError.WrappedTangemError -> { - if (error.code == USER_CANCELLED_ERROR_CODE) { - SendTransactionError.UserCancelledError - } else { - val tangemError = error.tangemError - if (tangemError is TangemSdkError) { - val resource = tangemError.localizedDescriptionRes() - val resId = resource.resId ?: R.string.common_unknown_error - val resArgs = resource.args.map { it.value } - val textReference = resourceReference(resId, wrappedList(resArgs)) - SendTransactionError.TangemSdkError(tangemError.code, textReference) - } else { - SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) - } - } - } + is BlockchainSdkError.WrappedTangemError -> parseWrappedError(error) is BlockchainSdkError.CreateAccountUnderfunded -> { val minAmount = error.minReserve val minValue = minAmount.value?.toFormattedString(minAmount.decimals).orEmpty() @@ -134,4 +123,26 @@ class SendTransactionUseCase( } } } + + private fun parseWrappedError(error: BlockchainSdkError.WrappedTangemError): SendTransactionError { + return if (error.code == USER_CANCELLED_ERROR_CODE) { + SendTransactionError.UserCancelledError + } else { + when (val tangemError = error.tangemError) { + is TangemSdkError -> { + val resource = tangemError.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + val textReference = resourceReference(resId, wrappedList(resArgs)) + SendTransactionError.TangemSdkError(tangemError.code, textReference) + } + is BlockchainSdkError.WrappedTangemError -> { + parseWrappedError(tangemError) // todo remove when sdk errors are revised + } + else -> { + SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) + } + } + } + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt index 4206d4854f..ff568c6794 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt @@ -8,25 +8,32 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext /** * Use case for rename user wallet * * @property userWalletsListManager user wallets list manager */ -class RenameWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class RenameWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val dispatchers: CoroutineDispatcherProvider, +) { suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either = - either { - val existingNames = userWalletsListManager.userWalletsSync + withContext(dispatchers.io) { + either { + val existingNames = userWalletsListManager.userWalletsSync - ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) { - UpdateWalletError.NameAlreadyExists - } + ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) { + UpdateWalletError.NameAlreadyExists + } - when (val result = userWalletsListManager.update(userWalletId) { it.copy(name = name) }) { - is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) - is CompletionResult.Success -> result.data + when (val result = userWalletsListManager.update(userWalletId) { it.copy(name = name) }) { + is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) + is CompletionResult.Success -> result.data + } } } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 7461386ac3..f14c204327 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -87,7 +87,7 @@ internal class ItemsBuilder @Inject constructor( DetailsItemUM.Basic.Item( id = "send_feedback", block = BlockUM( - text = resourceReference(R.string.details_send_feedback), + text = resourceReference(R.string.details_row_title_contact_to_support), iconRes = R.drawable.ic_comment_24, onClick = onClick, ), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt index eab5ed50e3..a9782fc1d9 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt @@ -21,7 +21,8 @@ internal fun List.toUiModels( isBalancesHidden: Boolean = false, ): ImmutableList = this.map { model -> val balance = balances[model.walletId] - model.mapToUiModel( + + model.toUiModel( balance = balance, appCurrency = appCurrency, isLoading = isLoading, @@ -30,7 +31,7 @@ internal fun List.toUiModels( ) }.toImmutableList() -private fun UserWallet.mapToUiModel( +private fun UserWallet.toUiModel( balance: TotalFiatBalance?, appCurrency: AppCurrency?, isLoading: Boolean, @@ -66,9 +67,9 @@ private fun UserWallet.getInfo( ) return when { + isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS)) isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked)) isLoading -> cardCountRef - isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS)) else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef) } } @@ -80,7 +81,7 @@ private fun getBalanceInfo( dividerRef: TextReference, ): TextReference { val amount = when (balance) { - is TotalFiatBalance.Loaded -> balance.amount.takeIf { balance.isAllAmountsSummarized } + is TotalFiatBalance.Loaded -> balance.amount is TotalFiatBalance.Failed, is TotalFiatBalance.Loading, null, diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt index f281084c55..5fbd86f488 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt @@ -7,7 +7,6 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.disclaimer.impl.model.DisclaimerModel import com.tangem.features.disclaimer.impl.ui.DisclaimerScreen @@ -18,7 +17,6 @@ import dagger.assisted.AssistedInject internal class DefaultDisclaimerComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: DisclaimerComponent.Params, - private val appFinisher: AppFinisher, ) : DisclaimerComponent, AppComponentContext by context { private val model: DisclaimerModel = getOrCreateModel(params) @@ -27,13 +25,7 @@ internal class DefaultDisclaimerComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() - BackHandler { - if (params.isTosAccepted) { - state.popBack() - } else { - appFinisher.finish() - } - } + BackHandler(onBack = state.popBack) DisclaimerScreen(state = state) } diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt index f1be4240e0..e2dffb37f3 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.settings.NeverRequestPermissionUseCase import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase @@ -17,12 +18,14 @@ import kotlinx.coroutines.launch import javax.inject.Inject @ComponentScoped +@Suppress("LongParameterList") internal class DisclaimerModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, private val cardRepository: CardRepository, private val router: Router, - override val dispatchers: CoroutineDispatcherProvider, private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, + private val appFinisher: AppFinisher, paramsContainer: ParamsContainer, ) : Model() { @@ -33,23 +36,34 @@ internal class DisclaimerModel @Inject constructor( onAccept = ::onAccept, url = DISCLAIMER_URL, isTosAccepted = params.isTosAccepted, - popBack = router::pop, + popBack = ::popBack, ), ) - private fun onAccept(shouldAskPushPermission: Boolean) { - modelScope.launch { + private fun onAccept(shouldAskPushPermission: Boolean) = modelScope.launch { + if (params.isTosAccepted) { + router.pop() + } else { cardRepository.acceptTangemTOS() + if (shouldAskPushPermission) { router.push(AppRoute.PushNotification) } else { neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) neverRequestPermissionUseCase(PUSH_PERMISSION) - router.push(AppRoute.Home) + router.replaceAll(AppRoute.Home) } } } + private fun popBack() { + if (params.isTosAccepted) { + router.pop() + } else { + appFinisher.finish() + } + } + private companion object { const val DISCLAIMER_URL = "https://tangem.com/tangem_tos.html" } diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt index c35b196ec8..ae6f90b7a1 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt @@ -17,19 +17,15 @@ sealed class PushNotificationAnalyticEvents( ), ) - data class ButtonLater( + data class ButtonCancel( val source: AnalyticsParam.ScreensSources, ) : PushNotificationAnalyticEvents( - event = "Button - Later", + event = "Button - Cancel", params = mapOf( AnalyticsParam.SOURCE to source.value, ), ) - data object ButtonCancel : PushNotificationAnalyticEvents( - event = "Button - Cancel", - ) - data class PermissionStatus( val isAllowed: Boolean, ) : PushNotificationAnalyticEvents( diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt index 1d6fb08e71..c7110e411f 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt @@ -26,10 +26,9 @@ internal class PushNotificationsFragment : ComposeFragment() { NavigationBar3ButtonsScrim() PushNotificationsScreen( onRequest = viewModel::onRequest, - onRequestLater = viewModel::onRequestLater, + onNeverRequest = viewModel::onNeverRequest, onAllowPermission = viewModel::onAllowPermission, onDenyPermission = viewModel::onDenyPermission, - onOpenSettings = viewModel::openSettings, ) } diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt index c039d9109d..a73e852c64 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt @@ -17,18 +17,15 @@ import kotlinx.collections.immutable.persistentListOf @Composable internal fun PushNotificationsScreen( onRequest: () -> Unit, - onRequestLater: () -> Unit, + onNeverRequest: () -> Unit, onAllowPermission: () -> Unit, onDenyPermission: () -> Unit, - onOpenSettings: () -> Unit, ) { val isClicked = remember { mutableStateOf(false) } val requestPushPermission = requestPushPermission( - isFirstTimeAsking = true, isClicked = isClicked, onAllow = onAllowPermission, onDeny = onDenyPermission, - onOpenSettings = onOpenSettings, pushPermission = getPushPermissionOrNull(), ) @@ -54,8 +51,8 @@ internal fun PushNotificationsScreen( }, ), secondaryButton = ShowcaseButtonModel( - buttonText = resourceReference(R.string.common_later), - onClick = onRequestLater, + buttonText = resourceReference(R.string.common_cancel), + onClick = onNeverRequest, ), modifier = Modifier.systemBarsPadding(), ) diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt index b87d3f4f88..5b62923bbb 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt @@ -4,11 +4,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.navigation.settings.SettingsManager -import com.tangem.domain.settings.DelayPermissionRequestUseCase import com.tangem.domain.settings.NeverRequestPermissionUseCase import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase -import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter @@ -19,12 +16,9 @@ import javax.inject.Inject @Suppress("LongParameterList") @HiltViewModel internal class PushNotificationViewModel @Inject constructor( - private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase, - private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase, private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, private val router: DefaultPushNotificationsRouter, - private val settingsManager: SettingsManager, private val analyticHandler: AnalyticsEventHandler, ) : ViewModel(), PushNotificationsClickIntents { @@ -32,18 +26,14 @@ internal class PushNotificationViewModel @Inject constructor( analyticHandler.send( PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Stories), ) - viewModelScope.launch { - setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) - } } - override fun onRequestLater() { + override fun onNeverRequest() { analyticHandler.send( - PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Stories), + PushNotificationAnalyticEvents.ButtonCancel(AnalyticsParam.ScreensSources.Stories), ) viewModelScope.launch { - delayPermissionRequestUseCase(PUSH_PERMISSION) - setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) + neverRequestPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) router.openHome() } @@ -65,11 +55,9 @@ internal class PushNotificationViewModel @Inject constructor( PushNotificationAnalyticEvents.PermissionStatus(isAllowed = false), ) viewModelScope.launch { - delayPermissionRequestUseCase(PUSH_PERMISSION) + neverRequestPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) router.openHome() } } - - override fun openSettings() = settingsManager.openSettings() } \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt index 2d4fe34217..63100190b7 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt @@ -3,11 +3,9 @@ package com.tangem.features.pushnotifications.impl.presentation.viewmodel internal interface PushNotificationsClickIntents { fun onRequest() - fun onRequestLater() + fun onNeverRequest() fun onAllowPermission() fun onDenyPermission() - - fun openSettings() } \ No newline at end of file diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts index 0ae67f2d5f..4206513db7 100644 --- a/features/referral/domain/build.gradle.kts +++ b/features/referral/domain/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(deps.arrow.core) implementation(deps.jodatime) implementation(deps.timber) + implementation(deps.tangem.card.core) /** DI */ implementation(deps.hilt.android) diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 36782f4e1d..04e3bb3028 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -1,10 +1,12 @@ package com.tangem.feature.referral.domain import arrow.core.getOrElse +import com.tangem.common.core.TangemSdkError import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.referral.domain.errors.ReferralError import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData import com.tangem.lib.crypto.UserWalletManager @@ -42,7 +44,7 @@ internal class ReferralInteractorImpl( val cryptoCurrency = repository.getCryptoCurrency(userWalletId = userWallet.walletId, tokenData = tokenData) derivePublicKeysUseCase(userWallet.walletId, listOfNotNull(cryptoCurrency)).getOrElse { Timber.e("Failed to derive public keys: $it") - throw it + throw it.mapToDomainError() } addCryptoCurrenciesUseCase( @@ -67,4 +69,13 @@ internal class ReferralInteractorImpl( tokensForReferral.clear() tokensForReferral.addAll(tokens) } + + private fun Throwable.mapToDomainError(): ReferralError { + if (this !is TangemSdkError) return ReferralError.DataError(this) + return if (this is TangemSdkError.UserCancelled) { + ReferralError.UserCancelledException + } else { + ReferralError.SdkError + } + } } \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt new file mode 100644 index 0000000000..8d78d31fdd --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.referral.domain.errors + +sealed class ReferralError : Exception() { + data object UserCancelledException : ReferralError() + data object SdkError : ReferralError() + + data class DataError(val throwable: Throwable) : ReferralError() +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt index 6565947793..236d0c93db 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.referral.analytics.ReferralEvents import com.tangem.feature.referral.domain.ReferralInteractor +import com.tangem.feature.referral.domain.errors.ReferralError import com.tangem.feature.referral.domain.models.DiscountType import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.ReferralInfo @@ -21,7 +22,6 @@ import com.tangem.feature.referral.models.ReferralStateHolder import com.tangem.feature.referral.models.ReferralStateHolder.ErrorSnackbar import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoState import com.tangem.feature.referral.router.ReferralRouter -import com.tangem.lib.crypto.models.errors.UserCancelledException import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching import dagger.hilt.android.lifecycle.HiltViewModel @@ -95,7 +95,7 @@ internal class ReferralViewModel @Inject constructor( runCatching(dispatchers.io) { referralInteractor.startReferral(userWalletId) } .onSuccess(::showContent) .onFailure { throwable -> - if (throwable is UserCancelledException) { + if (throwable is ReferralError.UserCancelledException) { lastReferralData?.let { referralData -> showContent(referralData) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index f7d431316d..6432bbbea7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -20,15 +20,10 @@ internal object InitialStakingStatePreview { endText = TextReference.Str("15 SOL"), ), RoundedListWithDividersItemData( - id = R.string.staking_details_apy, - startText = TextReference.Res(R.string.staking_details_apy), + id = R.string.staking_details_annual_percentage_rate, + startText = TextReference.Res(R.string.staking_details_annual_percentage_rate), endText = TextReference.Str("2.54-5.12%"), ), - RoundedListWithDividersItemData( - id = R.string.staking_details_on_stake, - startText = TextReference.Res(R.string.staking_details_on_stake), - endText = TextReference.Str("0 SOL"), - ), RoundedListWithDividersItemData( id = R.string.staking_details_unbonding_period, startText = TextReference.Res(R.string.staking_details_unbonding_period), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 1cecfa9476..a40933fac8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -9,7 +9,6 @@ import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R @@ -22,7 +21,6 @@ import com.tangem.features.staking.impl.presentation.state.converters.YieldBalan import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider -import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList @@ -91,7 +89,6 @@ internal class SetInitialDataStateTransformer( return listOfNotNull( createAvailableItem(cryptoCurrencyStatus), createApyItem(), - createOnStakeItem(cryptoCurrencyStatus, yieldBalance), createUnbondingPeriodItem(), createMinimumRequirementItem(cryptoCurrencyStatus), createRewardClaimingItem(), @@ -116,30 +113,13 @@ internal class SetInitialDataStateTransformer( private fun createApyItem(): RoundedListWithDividersItemData { return RoundedListWithDividersItemData( - id = R.string.staking_details_apy, - startText = TextReference.Res(R.string.staking_details_apy), + id = R.string.staking_details_annual_percentage_rate, + startText = TextReference.Res(R.string.staking_details_annual_percentage_rate), endText = getAprRange(), iconClick = { clickIntents.onInfoClick(InfoType.APY) }, ) } - private fun createOnStakeItem( - cryptoCurrencyStatus: CryptoCurrencyStatus, - yieldBalance: YieldBalance?, - ): RoundedListWithDividersItemData { - return RoundedListWithDividersItemData( - id = R.string.staking_details_on_stake, - startText = TextReference.Res(R.string.staking_details_on_stake), - endText = TextReference.Str( - value = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = (yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero(), - cryptoCurrency = cryptoCurrencyStatus.currency.symbol, - decimals = cryptoCurrencyStatus.currency.decimals, - ), - ), - ) - } - private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData { return RoundedListWithDividersItemData( id = R.string.staking_details_unbonding_period, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt index da8124ccf1..755ca4552d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt @@ -19,8 +19,8 @@ internal class ShowInfoBottomSheetStateTransformer( isShow = true, content = when (infoType) { InfoType.APY -> StakingInfoBottomSheetConfig( - title = resourceReference(R.string.staking_details_apy), - text = resourceReference(R.string.staking_details_apy_info), + title = resourceReference(R.string.staking_details_annual_percentage_rate), + text = resourceReference(R.string.staking_details_annual_percentage_rate_info), ) InfoType.UNBONDING_PERIOD -> StakingInfoBottomSheetConfig( title = resourceReference(R.string.staking_details_unbonding_period), diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt index 0a566d5500..d782b606f9 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt @@ -15,6 +15,8 @@ internal class ExchangeStatusConverter : Converter { @@ -24,19 +25,30 @@ internal class ExpressDataConverter : Converter = emptyList(), + val swapProvider: SwapProvider, ) : SwapState data class EmptyAmountState(val zeroAmountEquivalent: String) : SwapState @@ -102,6 +107,9 @@ data class TxFee( val gasLimit: Int, val feeFiatFormatted: String, val feeCryptoFormatted: String, + val feeIncludeOtherNativeFee: BigDecimal, + val feeFiatFormattedWithNative: String, + val feeCryptoFormattedWithNative: String, val decimals: Int, val cryptoSymbol: String, val feeType: FeeType, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 73dceaab1e..369fd439ad 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -7,6 +7,7 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal @@ -229,7 +230,6 @@ internal class SwapInteractorImpl @Inject constructor( permissionOptions.fromToken.network, permissionOptions.txFee.gasLimit, ), - isSwap = false, ).getOrElse { Timber.e(it, "Failed to create approveTransaction") return SwapTransactionState.UnknownError @@ -325,8 +325,8 @@ internal class SwapInteractorImpl @Inject constructor( val fromTokenAddress = getTokenAddress(fromToken.currency) val isAllowedToSpend = quotes.fold( - ifRight = { - it.allowanceContract?.let { + ifRight = { quotes -> + quotes.allowanceContract?.let { isAllowedToSpend(networkId, fromToken.currency, amount, it) } ?: true }, @@ -351,7 +351,7 @@ internal class SwapInteractorImpl @Inject constructor( ) } else { provider to getQuotesState( - exchangeProviderType = provider.type, + provider = provider, quoteDataModel = quotes, amount = amount, fromToken = fromToken, @@ -375,7 +375,6 @@ internal class SwapInteractorImpl @Inject constructor( isBalanceWithoutFeeEnough: Boolean, ): Pair { return provider to loadCexQuoteData( - exchangeProviderType = ExchangeProviderType.CEX, networkId = networkId, amount = amount, fromTokenStatus = fromToken, @@ -602,8 +601,8 @@ internal class SwapInteractorImpl @Inject constructor( swapData = requireNotNull(swapData), currencyToSendStatus = currencyToSend, currencyToGetStatus = currencyToGet, - amountToSwap = amountToSwap, fee = fee, + amountToSwap = amountToSwap, userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } @@ -629,8 +628,8 @@ internal class SwapInteractorImpl @Inject constructor( ) val fee = when (val txFee = state.txFee) { TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue - is TxFeeState.SingleFeeState -> txFee.fee.feeValue + is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeIncludeOtherNativeFee + is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee } val feeState = getFeeState( fee = fee, @@ -663,8 +662,9 @@ internal class SwapInteractorImpl @Inject constructor( val derivationPath = currencyToSendStatus.currency.network.derivationPath.value val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX val dataToSign = dexTransaction.txData + val amountToSend = createNativeAmountForDex(swapData.transaction.txValue, currencyToSendStatus.currency.network) val txData = createTransactionUseCase( - amount = amount.value.convertToAmount(currencyToSendStatus.currency), + amount = amountToSend, fee = getFeeForTransaction( fee = fee, blockchain = Blockchain.fromId(currencyToSendStatus.currency.network.id.value), @@ -675,7 +675,6 @@ internal class SwapInteractorImpl @Inject constructor( network = currencyToSendStatus.currency.network, txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, fee.gasLimit), hash = dataToSign, - isSwap = true, ).getOrElse { Timber.e(it, "Failed to create swap dex tx data") return SwapTransactionState.UnknownError @@ -877,7 +876,7 @@ internal class SwapInteractorImpl @Inject constructor( ) blockchain == Blockchain.Aptos -> { val gasUnitPrice = fee.feeValue.divide( - BigDecimal(fee.gasLimit), + fee.gasLimit.toBigDecimal(), Blockchain.Aptos.decimals(), RoundingMode.HALF_UP, ) @@ -1006,7 +1005,6 @@ internal class SwapInteractorImpl @Inject constructor( */ @Suppress("LongParameterList") private suspend fun loadCexQuoteData( - exchangeProviderType: ExchangeProviderType, networkId: String, amount: SwapAmount, fromTokenStatus: CryptoCurrencyStatus, @@ -1057,7 +1055,7 @@ internal class SwapInteractorImpl @Inject constructor( ) getQuotesState( - exchangeProviderType = exchangeProviderType, + provider = provider, quoteDataModel = quotes, amount = amount, fromToken = fromTokenStatus, @@ -1074,7 +1072,7 @@ internal class SwapInteractorImpl @Inject constructor( @Suppress("LongMethod") private suspend fun getQuotesState( - exchangeProviderType: ExchangeProviderType, + provider: SwapProvider, quoteDataModel: Either, amount: SwapAmount, fromToken: CryptoCurrencyStatus, @@ -1096,6 +1094,7 @@ internal class SwapInteractorImpl @Inject constructor( toTokenAmount = quoteModel.toTokenAmount, swapData = null, txFeeState = txFee, + provider = provider, ).copy( warnings = manageWarnings( fromTokenStatus = fromToken, @@ -1105,7 +1104,7 @@ internal class SwapInteractorImpl @Inject constructor( ), ) - when (exchangeProviderType) { + when (provider.type) { ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { val state = updatePermissionState( networkId = networkId, @@ -1169,8 +1168,8 @@ internal class SwapInteractorImpl @Inject constructor( ): IncludeFeeInAmount { val feeValue = when (txFee) { TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue - is TxFeeState.SingleFeeState -> txFee.fee.feeValue + is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeIncludeOtherNativeFee + is TxFeeState.SingleFeeState -> txFee.fee.feeIncludeOtherNativeFee } val feePaidCurrency = getFeePaidCurrency( userWalletId = requireNotNull(getSelectedWallet()).walletId, @@ -1276,35 +1275,43 @@ internal class SwapInteractorImpl @Inject constructor( providerId = provider.providerId, rateType = RateType.FLOAT, toAddress = toToken.value.networkAddress?.defaultAddress?.value.orEmpty(), + refundAddress = fromToken.value.networkAddress?.defaultAddress?.value, ).fold( ifRight = { swapData -> + val transaction = swapData.transaction as ExpressTransactionModel.DEX + val nativeCoinDecimals = Blockchain.fromNetworkId(networkId)?.decimals() + ?: error("Blockchain not found") + val otherNativeFee = transaction.otherNativeFeeWei + ?.movePointLeft(nativeCoinDecimals) + ?: BigDecimal.ZERO val userWallet = getSelectedWallet() - val cardId = userWallet?.scanResponse?.card?.cardId - val feeData = if (cardId != null && isDemoCardUseCase(cardId)) { - getDemoFees(fromToken.currency) - } else { - transactionManager.getFee( + val txFeeState = when ( + val feeData = getFeeDataForDexSwap( networkId = networkId, - amountToSend = amount.value, - currencyToSend = swapCurrencyConverter.convert(fromToken.currency), - destinationAddress = swapData.transaction.txTo, - increaseBy = INCREASE_GAS_LIMIT_BY, - data = (swapData.transaction as ExpressTransactionModel.DEX).txData, - derivationPath = fromToken.currency.network.derivationPath.value, + transaction = transaction, + fromToken = fromToken.currency, + cardId = userWallet?.scanResponse?.card?.cardId, ) - } - val txFeeState = when (feeData) { - is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency) - is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken.currency) + ) { + is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee) + is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee) } val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState) - val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeByPriority) + val feeToCheckFunds = feeByPriority + (otherNativeFee ?: BigDecimal.ZERO) + val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeToCheckFunds) val feeState = getFeeState( - fee = feeByPriority, + fee = feeToCheckFunds, spendAmount = amount, networkId = networkId, fromTokenStatus = fromToken, ) + val preparedSwapConfigState = PreparedSwapConfigState( + isAllowedToSpend = true, + isBalanceEnough = isBalanceIncludeFeeEnough, + feeState = feeState, + hasOutgoingTransaction = hasOutgoingTransaction(fromToken), + includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex + ) val swapState = updateBalances( networkId = networkId, fromTokenStatus = fromToken, @@ -1313,6 +1320,7 @@ internal class SwapInteractorImpl @Inject constructor( toTokenAmount = swapData.toTokenAmount, swapData = swapData, txFeeState = txFeeState, + provider = provider, ) swapState.copy( permissionState = PermissionDataState.Empty, @@ -1320,17 +1328,9 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenStatus = fromToken, amount = amount, feeState = txFeeState, - minAdaValue = (feeData as? ProxyFees.SingleFee)?.let { - (it.singleFee as? ProxyFee.CardanoToken)?.minAdaValue - }, - ), - preparedSwapConfigState = PreparedSwapConfigState( - isAllowedToSpend = true, - isBalanceEnough = isBalanceIncludeFeeEnough, - feeState = feeState, - hasOutgoingTransaction = hasOutgoingTransaction(fromToken), - includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex + minAdaValue = null, // no ADA in DEX ), + preparedSwapConfigState = preparedSwapConfigState, ) }, ifLeft = { error -> @@ -1350,8 +1350,46 @@ internal class SwapInteractorImpl @Inject constructor( ) } + private suspend fun getFeeDataForDexSwap( + networkId: String, + transaction: ExpressTransactionModel.DEX, + fromToken: CryptoCurrency, + cardId: String?, + ): ProxyFees { + if (cardId != null && isDemoCardUseCase(cardId)) { + return getDemoFees(fromToken) + } + return try { + val nativeBalance = userWalletManager.getNativeTokenBalance( + networkId = networkId, + derivationPath = fromToken.network.derivationPath.value, + ) ?: ProxyAmount.empty() + val amountToSend = createNativeAmountForDex(transaction.txValue, fromToken.network) + // transaction.txValue is always native coin + if (nativeBalance.value < amountToSend.value) { + error("It's impossible to calculate fee for nativeBalance.value < amountToSend.value") + } + transactionManager.getFee( + networkId = networkId, + amountToSend = amountToSend, + currencyToSend = swapCurrencyConverter.convert(fromToken), + destinationAddress = transaction.txTo, + increaseBy = INCREASE_GAS_LIMIT_BY, + data = transaction.txData, + derivationPath = fromToken.network.derivationPath.value, + ) + } catch (e: IllegalStateException) { + transactionManager.getFeeForGas( + networkId = networkId, + gas = transaction.gas.multiply(INCREASE_GAS_LIMIT_BY.toBigInteger()).divide(100.toBigInteger()), + derivationPath = fromToken.network.derivationPath.value, + ) + } + } + @Suppress("LongParameterList") private suspend fun updateBalances( + provider: SwapProvider, networkId: String, fromTokenStatus: CryptoCurrencyStatus, toTokenStatus: CryptoCurrencyStatus, @@ -1363,7 +1401,6 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency val nativeToken = repository.getNativeTokenForNetwork(networkId) - val rates = getQuotes(fromToken.id, toToken.id, nativeToken.id) return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( @@ -1386,6 +1423,7 @@ internal class SwapInteractorImpl @Inject constructor( ), swapDataModel = swapData, txFee = txFeeState, + swapProvider = provider, ) } @@ -1395,7 +1433,7 @@ internal class SwapInteractorImpl @Inject constructor( ): TxFeeState { return txFeeResult?.fold( ifLeft = { TxFeeState.Empty }, - ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency) }, + ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency, null) }, ) ?: TxFeeState.Empty } @@ -1455,7 +1493,7 @@ internal class SwapInteractorImpl @Inject constructor( try { transactionManager.getFee( networkId = networkId, - amountToSend = BigDecimal.ZERO, + amountToSend = createNativeAmountForDex("0", fromToken.network), currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)), destinationAddress = fromToken.getContractAddress(), increaseBy = INCREASE_GAS_LIMIT_BY, @@ -1503,11 +1541,16 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState(fromToken: CryptoCurrency): TxFeeState { + private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState( + fromToken: CryptoCurrency, + otherNativeFee: BigDecimal? = null, + ): TxFeeState { + val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO val normalFeeValue = this.minFee.fee.value // in swap for normal use min fee val normalFeeGas = this.minFee.gasLimit.toInt() val priorityFeeValue = this.normalFee.fee.value // in swap for priority use normal fee val priorityFeeGas = this.normalFee.gasLimit.toInt() + // region fees to use val feesFiat = getFormattedFiatFees(fromToken, normalFeeValue, priorityFeeValue) val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" } val priorityFiatFee = requireNotNull(feesFiat.getOrNull(1)) { "feesFiat item 1 couldn't be null" } @@ -1519,12 +1562,35 @@ internal class SwapInteractorImpl @Inject constructor( amount = priorityFeeValue, decimals = normalFee.fee.decimals, ) + // endregion + // region fees include otherNativeFee + val feesFiatWithNative = getFormattedFiatFees( + fromToken = fromToken, + normalFeeValue + otherNativeFeeValue, + priorityFeeValue + otherNativeFeeValue, + ) + val normalFiatFeeWithNative = + requireNotNull(feesFiatWithNative.getOrNull(0)) { "feesFiat item 0 couldn't be null" } + val priorityFiatFeeWithNative = + requireNotNull(feesFiatWithNative.getOrNull(1)) { "feesFiat item 1 couldn't be null" } + val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = normalFeeValue + otherNativeFeeValue, + decimals = minFee.fee.decimals, + ) + val priorityCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = priorityFeeValue + otherNativeFeeValue, + decimals = normalFee.fee.decimals, + ) + // endregion return TxFeeState.MultipleFeeState( normalFee = TxFee( feeValue = normalFeeValue, gasLimit = normalFeeGas, feeFiatFormatted = normalFiatFee, feeCryptoFormatted = normalCryptoFee, + feeIncludeOtherNativeFee = normalFeeValue + otherNativeFeeValue, + feeFiatFormattedWithNative = normalFiatFeeWithNative, + feeCryptoFormattedWithNative = normalCryptoFeeWithNative, decimals = minFee.fee.decimals, cryptoSymbol = minFee.fee.currencySymbol, feeType = FeeType.NORMAL, @@ -1535,6 +1601,9 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = priorityFeeGas, feeFiatFormatted = priorityFiatFee, feeCryptoFormatted = priorityCryptoFee, + feeIncludeOtherNativeFee = priorityFeeValue + otherNativeFeeValue, + feeFiatFormattedWithNative = priorityFiatFeeWithNative, + feeCryptoFormattedWithNative = priorityCryptoFeeWithNative, decimals = normalFee.fee.decimals, cryptoSymbol = normalFee.fee.currencySymbol, feeType = FeeType.PRIORITY, @@ -1543,7 +1612,11 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState(fromToken: CryptoCurrency): TxFeeState { + private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState( + fromToken: CryptoCurrency, + otherNativeFee: BigDecimal? = null, + ): TxFeeState { + val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO val normalFeeValue = this.singleFee.fee.value val normalFeeGas = this.singleFee.gasLimit.toInt() val feesFiat = getFormattedFiatFees(fromToken, normalFeeValue) @@ -1552,12 +1625,28 @@ internal class SwapInteractorImpl @Inject constructor( amount = normalFeeValue, decimals = singleFee.fee.decimals, ) + // region fees include otherNativeFee + val feesFiatWithNative = getFormattedFiatFees( + fromToken = fromToken, + normalFeeValue + otherNativeFeeValue, + normalFeeValue + otherNativeFeeValue, + ) + val normalFiatFeeWithNative = + requireNotNull(feesFiatWithNative.getOrNull(0)) { "feesFiat item 0 couldn't be null" } + val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = normalFeeValue + otherNativeFeeValue, + decimals = singleFee.fee.decimals, + ) + // endregion return TxFeeState.SingleFeeState( fee = TxFee( feeValue = normalFeeValue, gasLimit = normalFeeGas, feeFiatFormatted = normalFiatFee, feeCryptoFormatted = normalCryptoFee, + feeIncludeOtherNativeFee = normalFeeValue + otherNativeFeeValue, + feeFiatFormattedWithNative = normalFiatFeeWithNative, + feeCryptoFormattedWithNative = normalCryptoFeeWithNative, decimals = singleFee.fee.decimals, cryptoSymbol = singleFee.fee.currencySymbol, feeType = FeeType.NORMAL, @@ -1566,7 +1655,12 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun TransactionFee.toTxFeeState(fromToken: CryptoCurrency): TxFeeState { + @Suppress("LongMethod") + private suspend fun TransactionFee.toTxFeeState( + fromToken: CryptoCurrency, + otherNativeFee: BigDecimal?, + ): TxFeeState { + val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO return when (this) { is TransactionFee.Choosable -> { val normalFee = this.normal.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND) @@ -1584,12 +1678,31 @@ internal class SwapInteractorImpl @Inject constructor( amount = feePriority, decimals = priorityFee.amount.decimals, ) + + // region otherNativeFee + val normalFeeWithOtherNative = feeNormal + otherNativeFeeValue + val priorityFeeWithOtherNative = feePriority + otherNativeFeeValue + val normalFiatValueWithNative = getFormattedFiatFees(fromToken, normalFeeWithOtherNative)[0] + val priorityFiatValueWithNative = getFormattedFiatFees(fromToken, priorityFeeWithOtherNative)[0] + + val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = normalFeeWithOtherNative, + decimals = normalFee.amount.decimals, + ) + val priorityCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = priorityFeeWithOtherNative, + decimals = priorityFee.amount.decimals, + ) + // endregion TxFeeState.MultipleFeeState( normalFee = TxFee( feeValue = feeNormal, gasLimit = normalFee.getGasLimit(), feeFiatFormatted = normalFiatValue, feeCryptoFormatted = normalCryptoFee, + feeIncludeOtherNativeFee = normalFeeWithOtherNative, + feeFiatFormattedWithNative = normalFiatValueWithNative, + feeCryptoFormattedWithNative = normalCryptoFeeWithNative, decimals = normalFee.amount.decimals, cryptoSymbol = normalFee.amount.currencySymbol, feeType = FeeType.NORMAL, @@ -1600,6 +1713,9 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = priorityFee.getGasLimit(), feeFiatFormatted = priorityFiatValue, feeCryptoFormatted = priorityCryptoFee, + feeIncludeOtherNativeFee = priorityFeeWithOtherNative, + feeFiatFormattedWithNative = priorityFiatValueWithNative, + feeCryptoFormattedWithNative = priorityCryptoFeeWithNative, decimals = priorityFee.amount.decimals, cryptoSymbol = priorityFee.amount.currencySymbol, feeType = FeeType.PRIORITY, @@ -1614,12 +1730,24 @@ internal class SwapInteractorImpl @Inject constructor( amount = feeNormal, decimals = this.normal.amount.decimals, ) + // region otherNativeFee + val normalFeeWithOtherNative = feeNormal + otherNativeFeeValue + val normalFiatValueWithNative = getFormattedFiatFees(fromToken, normalFeeWithOtherNative)[0] + + val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( + amount = normalFeeWithOtherNative, + decimals = this.normal.amount.decimals, + ) + // endregion TxFeeState.SingleFeeState( fee = TxFee( feeValue = this.normal.amount.value ?: BigDecimal.ZERO, gasLimit = this.normal.getGasLimit(), feeFiatFormatted = normalFiatValue, feeCryptoFormatted = normalCryptoFee, + feeIncludeOtherNativeFee = normalFeeWithOtherNative, + feeFiatFormattedWithNative = normalFiatValueWithNative, + feeCryptoFormattedWithNative = normalCryptoFeeWithNative, decimals = normal.amount.decimals, cryptoSymbol = normal.amount.currencySymbol, feeType = FeeType.NORMAL, @@ -1630,6 +1758,18 @@ internal class SwapInteractorImpl @Inject constructor( } } + private fun createNativeAmountForDex(txValueAmount: String, network: Network): Amount { + val nativeDecimals = Blockchain.fromNetworkId(network.backendId)?.decimals() + ?: error("Blockchain not found") + val decimalValue = txValueAmount.toBigDecimalOrNull()?.movePointLeft(nativeDecimals) + ?: error("txValue parse error") + return Amount( + currencySymbol = network.currencySymbol, + value = decimalValue, + decimals = nativeDecimals, + ) + } + /** * Workaround to increase gas limit cause we calculate fee for random address */ @@ -1691,7 +1831,7 @@ internal class SwapInteractorImpl @Inject constructor( if (fromToken.currency is CryptoCurrency.Token) { tokenBalance >= amount.value } else { - tokenBalance > amount.value.plus(fee ?: BigDecimal.ZERO) + tokenBalance >= amount.value.plus(fee ?: BigDecimal.ZERO) } } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index f901691c85..003f258f6f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.models import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.core.ui.R @@ -75,18 +74,21 @@ data class SwapButton( sealed interface TransactionCardType { - val headerResId: Int + val header: TextReference + val isError: Boolean data class Inputtable( val onAmountChanged: ((String) -> Unit), val onFocusChanged: ((Boolean) -> Unit), - @StringRes override val headerResId: Int = R.string.swapping_from_title, + override val isError: Boolean, + override val header: TextReference = TextReference.Res(R.string.swapping_from_title), ) : TransactionCardType data class ReadOnly( val showWarning: Boolean = false, val onWarningClick: (() -> Unit)? = null, - @StringRes override val headerResId: Int = R.string.swapping_to_title, + override val isError: Boolean = false, + override val header: TextReference = TextReference.Res(R.string.swapping_to_title), ) : TransactionCardType } @@ -103,7 +105,7 @@ data class LegalState( sealed interface SwapWarning { data class PermissionNeeded(val notificationConfig: NotificationConfig) : SwapWarning - object InsufficientFunds : SwapWarning + data object InsufficientFunds : SwapWarning data class NoAvailableTokensToSwap(val notificationConfig: NotificationConfig) : SwapWarning data class GenericWarning( val title: TextReference? = null, 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 c9ad194413..517c795db6 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 @@ -54,7 +54,11 @@ internal class StateBuilder( return SwapStateHolder( blockchainId = networkInfo.blockchainId, sendCardData = SwapCardState.SwapCardData( - type = TransactionCardType.Inputtable(actions.onAmountChanged, actions.onAmountSelected), + type = TransactionCardType.Inputtable( + onAmountChanged = actions.onAmountChanged, + onFocusChanged = actions.onAmountSelected, + isError = false, + ), amountEquivalent = null, amountTextFieldValue = null, token = null, @@ -155,9 +159,13 @@ internal class StateBuilder( val canSelectReceiveToken = mainTokenId != toToken.id.value if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder + val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( + isError = false, + header = TextReference.Res(R.string.swapping_from_title), + ) return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( - type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), + type = sendInput, amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = null, token = uiStateHolder.sendCardData.token, @@ -225,9 +233,19 @@ internal class StateBuilder( val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus + val isInsufficientFunds = isInsufficientFundsCondition(quoteModel) + val insufficientFundsHeader = if (isInsufficientFunds) { + TextReference.Res(R.string.swapping_insufficient_funds) + } else { + TextReference.Res(R.string.swapping_from_title) + } + val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( + isError = isInsufficientFunds, + header = insufficientFundsHeader, + ) return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( - type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), + type = sendInput, amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = getFormattedFiatAmount(quoteModel.fromTokenInfo.amountFiat), token = fromCurrencyStatus, @@ -486,7 +504,7 @@ internal class StateBuilder( ) { warnings.add( SwapWarning.PermissionNeeded( - createPermissionNotificationConfig(providerName, fromToken.symbol), + createPermissionNotificationConfig(fromToken.symbol, providerName), ), ) } @@ -504,8 +522,8 @@ internal class StateBuilder( warnings.add( SwapWarning.GeneralWarning( createNetworkFeeCoverageNotificationConfig( - fee.feeCryptoFormatted, - fee.feeFiatFormatted, + fee.feeCryptoFormattedWithNative, + fee.feeFiatFormattedWithNative, ), ), ) @@ -558,13 +576,16 @@ internal class StateBuilder( warnings: MutableList, ) { // check isBalanceEnough, but for dex includeFeeInAmount always Excluded - if (!quoteModel.preparedSwapConfigState.isBalanceEnough && - quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included - ) { + if (isInsufficientFundsCondition(quoteModel)) { warnings.add(SwapWarning.InsufficientFunds) } } + private fun isInsufficientFundsCondition(quoteModel: SwapState.QuotesLoadedState): Boolean { + return !quoteModel.preparedSwapConfigState.isBalanceEnough && + quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included + } + private fun getSwapButtonEnabled(quoteModel: SwapState.QuotesLoadedState): Boolean { val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value if (status is CryptoCurrencyStatus.NoAccount) { @@ -975,9 +996,9 @@ internal class StateBuilder( return FeeItemState.Content( feeType = feeType, title = resourceReference(R.string.common_network_fee_title), - amountCrypto = fee.feeCryptoFormatted, + amountCrypto = fee.feeCryptoFormattedWithNative, // display fee with native as workaround for okx symbolCrypto = fee.cryptoSymbol, - amountFiatFormatted = fee.feeFiatFormatted, + amountFiatFormatted = fee.feeFiatFormattedWithNative, // display fee with native as workaround for okx isClickable = isClickable, onClick = actions.onClickFee, ) @@ -1017,8 +1038,7 @@ internal class StateBuilder( val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount)) val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount)) - val shouldShowStatus = providerState.type == ExchangeProviderType.CEX.providerName || - providerState.type == ExchangeProviderType.DEX_BRIDGE.providerName + val shouldShowStatus = providerState.type == ExchangeProviderType.CEX.providerName return uiState.copy( successState = SwapSuccessStateHolder( timestamp = swapTransactionState.timestamp, @@ -1028,7 +1048,7 @@ internal class StateBuilder( showStatusButton = shouldShowStatus, providerIcon = providerState.iconUrl, rate = providerState.subtitle, - fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"), + fee = stringReference("${fee.feeCryptoFormattedWithNative} (${fee.feeFiatFormattedWithNative})"), fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()), toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()), fromTokenFiatAmount = stringReference(fromFiatAmount), @@ -1375,18 +1395,18 @@ internal class StateBuilder( FeeItemState.Content( feeType = this.normalFee.feeType, title = resourceReference(R.string.common_network_fee_title), - amountCrypto = this.normalFee.feeCryptoFormatted, + amountCrypto = this.normalFee.feeCryptoFormattedWithNative, symbolCrypto = this.normalFee.cryptoSymbol, - amountFiatFormatted = this.normalFee.feeFiatFormatted, + amountFiatFormatted = this.normalFee.feeFiatFormattedWithNative, isClickable = true, onClick = {}, ), FeeItemState.Content( feeType = this.priorityFee.feeType, title = resourceReference(R.string.common_network_fee_title), - amountCrypto = this.priorityFee.feeCryptoFormatted, + amountCrypto = this.priorityFee.feeCryptoFormattedWithNative, symbolCrypto = this.priorityFee.cryptoSymbol, - amountFiatFormatted = this.priorityFee.feeFiatFormatted, + amountFiatFormatted = this.priorityFee.feeFiatFormattedWithNative, isClickable = true, onClick = {}, ), @@ -1419,7 +1439,7 @@ internal class StateBuilder( } // region warnings - private fun createPermissionNotificationConfig(providerName: String, fromTokenSymbol: String): NotificationConfig { + private fun createPermissionNotificationConfig(fromTokenSymbol: String, providerName: String): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.express_provider_permission_needed), subtitle = resourceReference( @@ -1621,7 +1641,7 @@ internal class StateBuilder( id = this.providerId, name = this.name, iconUrl = this.imageLarge, - type = this.type.toString(), + type = this.type.providerName, selectionType = selectionType, alertText = alertText, onProviderClick = onProviderClick, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 4dd7a9395f..f3004d1c73 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -435,7 +435,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U // region preview private val sendCard = SwapCardState.SwapCardData( - type = TransactionCardType.Inputtable({}, {}), + type = TransactionCardType.Inputtable({}, {}, false), amountTextFieldValue = TextFieldValue(), amountEquivalent = "1 000 000", tokenIconUrl = "", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 7ae3edc6a2..9baf09e90a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -37,6 +37,7 @@ import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.* +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.ImageBackgroundContrastChecker @@ -185,10 +186,14 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - val title = type.headerResId + val titleColor = if (type.isError) { + TangemTheme.colors.text.warning + } else { + TangemTheme.colors.text.tertiary + } Text( - text = stringResource(id = title), - color = TangemTheme.colors.text.tertiary, + text = type.header.resolveReference(), + color = titleColor, maxLines = 1, style = MaterialTheme.typography.subtitle2, modifier = Modifier @@ -546,7 +551,7 @@ private fun Preview_TransactionCardWithoutPriceImpact_InDarkTheme() { @Composable private fun TransactionCardPreview() { TransactionCard( - type = TransactionCardType.Inputtable({}, {}), + type = TransactionCardType.Inputtable({}, {}, false), amountEquivalent = "1 000 000", tokenIconUrl = "", tokenCurrency = "DAI", diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index 52f0679e6c..26daa6490f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -29,6 +29,7 @@ internal data class SwapTransactionsState( val fromFiatAmount: String, val fromCurrencyIcon: CurrencyIconState, val showProviderLink: Boolean, + val isRefundTerminalStatus: Boolean = true, val onClick: () -> Unit, val onGoToProviderClick: (String) -> Unit, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt index ac825983b8..47508d351e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt @@ -1,38 +1,92 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.components import androidx.compose.runtime.Immutable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.notifications.CurrencyNotificationConfig import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.tokendetails.impl.R @Immutable -internal sealed class ExchangeStatusNotifications(val config: NotificationConfig) { +internal sealed interface ExchangeStatusNotifications { - data class NeedVerification( - val onGoToProviderClick: () -> Unit, - ) : ExchangeStatusNotifications( + sealed class CommonNotification(val config: NotificationConfig) : ExchangeStatusNotifications + + data class NeedVerification(val onGoToProviderClick: () -> Unit) : CommonNotification( config = NotificationConfig( - title = TextReference.Res(R.string.express_exchange_notification_verification_title), - subtitle = TextReference.Res(R.string.express_exchange_notification_verification_text), + title = resourceReference(R.string.express_exchange_notification_verification_title), + subtitle = resourceReference(R.string.express_exchange_notification_verification_text), iconResId = R.drawable.ic_alert_triangle_20, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = TextReference.Res(R.string.common_go_to_provider), + text = resourceReference(R.string.common_go_to_provider), onClick = onGoToProviderClick, ), ), ) - data class Failed( - val onGoToProviderClick: () -> Unit, - ) : ExchangeStatusNotifications( + data class Failed(val onGoToProviderClick: () -> Unit) : CommonNotification( config = NotificationConfig( - title = TextReference.Res(R.string.express_exchange_notification_failed_title), - subtitle = TextReference.Res(R.string.express_exchange_notification_failed_text), + title = resourceReference(R.string.express_exchange_notification_failed_title), + subtitle = resourceReference(R.string.express_exchange_notification_failed_text), iconResId = R.drawable.ic_alert_circle_24, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = TextReference.Res(R.string.common_go_to_provider), + text = resourceReference(R.string.common_go_to_provider), onClick = onGoToProviderClick, ), ), ) + + data class TokenRefunded( + val cryptoCurrency: CryptoCurrency, + val onReadMoreClick: () -> Unit, + val onGoToTokenClick: () -> Unit, + ) : ExchangeStatusNotifications { + + val config = CurrencyNotificationConfig( + title = resourceReference( + id = R.string.express_exchange_notification_refund_title, + formatArgs = wrappedList(cryptoCurrency.symbol, cryptoCurrency.network.name), + ), + subtitle = CurrencyNotificationConfig.AnnotatedSubtitle( + valueProvider = { + val linkText = stringResource(R.string.common_read_more) + val fullString = stringResource( + R.string.express_exchange_notification_refund_text, + cryptoCurrency.symbol, + linkText, + ) + + val linkTextPosition = fullString.length - linkText.length + + buildAnnotatedString { + withStyle(SpanStyle(TangemTheme.colors.text.tertiary)) { + append(fullString.substring(0, linkTextPosition)) + } + + withStyle(SpanStyle(TangemTheme.colors.text.accent)) { + append(fullString.substring(linkTextPosition, fullString.length)) + } + } + }, + onClick = { value, position -> + val readMoreStyle = requireNotNull(value.spanStyles.getOrNull(1)) + if (position in readMoreStyle.start..readMoreStyle.end) { + onReadMoreClick() + } + }, + ), + tokenIconState = CryptoCurrencyToIconStateConverter().convert(cryptoCurrency), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_go_to_token), + onClick = onGoToTokenClick, + ), + ) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index a0231a352a..15523c353c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -25,6 +25,7 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal +import java.util.Locale internal class TokenDetailsSwapTransactionsStateConverter( private val clickIntents: TokenDetailsClickIntents, @@ -62,7 +63,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( }?.fiatRate?.multiply(fromAmount) val timestamp = transaction.timestamp val notifications = - getNotification(transaction.status?.status, transaction.status?.txExternalUrl) + getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null) val showProviderLink = getShowProviderLink(notifications, transaction.status) result.add( SwapTransactionsState( @@ -77,10 +78,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( statuses = getStatuses(transaction.status?.status), hasFailed = transaction.status?.status == ExchangeStatus.Failed, activeStatus = transaction.status?.status, - notification = getNotification( - transaction.status?.status, - transaction.status?.txExternalUrl, - ), + notification = notifications, toCryptoCurrency = toCryptoCurrency, toCryptoAmount = BigDecimalFormatter.formatCryptoAmount( cryptoAmount = toAmount, @@ -110,10 +108,15 @@ internal class TokenDetailsSwapTransactionsStateConverter( return result.toPersistentList() } - fun updateTxStatus(tx: SwapTransactionsState, statusModel: ExchangeStatusModel?): SwapTransactionsState { + fun updateTxStatus( + tx: SwapTransactionsState, + statusModel: ExchangeStatusModel?, + refundToken: CryptoCurrency?, + isRefundTerminalStatus: Boolean, + ): SwapTransactionsState { if (statusModel == null || tx.activeStatus == statusModel.status) return tx val hasFailed = tx.hasFailed || statusModel.status == ExchangeStatus.Failed - val notifications = getNotification(statusModel.status, statusModel.txExternalUrl) + val notifications = getNotification(statusModel.status, statusModel.txExternalUrl, refundToken) val showProviderLink = getShowProviderLink(notifications, statusModel) return tx.copy( activeStatus = statusModel.status, @@ -122,6 +125,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( statuses = getStatuses(statusModel.status, hasFailed), txUrl = statusModel.txExternalUrl, showProviderLink = showProviderLink, + isRefundTerminalStatus = isRefundTerminalStatus, ) } @@ -133,10 +137,14 @@ internal class TokenDetailsSwapTransactionsStateConverter( ) } - private fun getNotification(status: ExchangeStatus?, txUrl: String?): ExchangeStatusNotifications? { - if (txUrl == null) return null + private fun getNotification( + status: ExchangeStatus?, + txUrl: String?, + refundToken: CryptoCurrency?, + ): ExchangeStatusNotifications? { return when (status) { ExchangeStatus.Failed -> { + if (txUrl == null) return null ExchangeStatusNotifications.Failed { analyticsEventsHandlerProvider().send( TokenExchangeAnalyticsEvent.GoToProviderFail(cryptoCurrency.symbol), @@ -145,6 +153,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( } } ExchangeStatus.Verifying -> { + if (txUrl == null) return null ExchangeStatusNotifications.NeedVerification { analyticsEventsHandlerProvider().send( TokenExchangeAnalyticsEvent.GoToProviderKYC(cryptoCurrency.symbol), @@ -152,6 +161,17 @@ internal class TokenDetailsSwapTransactionsStateConverter( clickIntents.onGoToProviderClick(txUrl) } } + ExchangeStatus.Refunded -> { + if (refundToken == null) { + null + } else { + ExchangeStatusNotifications.TokenRefunded( + cryptoCurrency = refundToken, + onReadMoreClick = { clickIntents.onOpenUrlClick(url = getAboutCrossChainBridgesLink()) }, + onGoToTokenClick = { clickIntents.onGoToRefundedTokenClick(refundToken) }, + ) + } + } else -> null } } @@ -287,7 +307,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( status = ExchangeStatus.Refunded, text = TextReference.Res(R.string.express_exchange_status_refunded), isActive = false, - isDone = isRefunded, + isDone = false, ) else -> ExchangeStatusState( status = ExchangeStatus.Sending, @@ -300,4 +320,12 @@ internal class TokenDetailsSwapTransactionsStateConverter( isDone = isSendingDone, ) } + + private fun getAboutCrossChainBridgesLink(): String { + return if (Locale.getDefault().country == "RU") { + "https://tangem.com/ru/blog/post/an-overview-of-cross-chain-bridges/" + } else { + "https://tangem.com/en/blog/post/an-overview-of-cross-chain-bridges/" + } + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index c715a6c6bd..ff2dd3baff 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -115,6 +115,11 @@ private fun ExchangeStatusStep( color = TangemTheme.colors.icon.warning, isDone = it.isDone, ) + it.status == ExchangeStatus.Refunded -> ExchangeStep( + iconRes = R.drawable.ic_close_24, + color = TangemTheme.colors.icon.warning, + isDone = it.isDone, + ) it.status == ExchangeStatus.Verifying -> ExchangeStep( iconRes = R.drawable.ic_exclamation_24, color = TangemTheme.colors.icon.attention, @@ -141,6 +146,7 @@ private fun ExchangeStatusStep( private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) { val textColor = when { stepStatus.status == ExchangeStatus.Cancelled -> TangemTheme.colors.icon.warning + stepStatus.status == ExchangeStatus.Refunded -> TangemTheme.colors.icon.warning stepStatus.status == ExchangeStatus.Failed && !stepStatus.isDone -> TangemTheme.colors.icon.warning stepStatus.status == ExchangeStatus.Verifying && !stepStatus.isDone -> TangemTheme.colors.icon.attention stepStatus.isDone -> TangemTheme.colors.text.primary1 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index a1a7c595a5..d3372936b1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -17,11 +17,12 @@ import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.CurrencyNotification import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications @Composable internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) { @@ -29,16 +30,13 @@ internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) { config = config, containerColor = TangemTheme.colors.background.tertiary, ) { content: ExchangeStatusBottomSheetConfig -> - ExchangeStatusBottomSheetContent(content = content) + ExchangeStatusBottomSheetContent(config = content.value) } } @Composable -private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) { - val config = content.value - Column( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), - ) { +private fun ExchangeStatusBottomSheetContent(config: SwapTransactionsState) { + Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { SpacerH10() Text( text = stringResource(id = R.string.express_exchange_status_title), @@ -80,25 +78,39 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC showLink = config.showProviderLink, onClick = { config.onGoToProviderClick(config.txUrl.orEmpty()) }, ) - AnimatedContent( - targetState = config.notification, - label = "Exchange Status Notification Change", - ) { - it?.let { - val tint = when (config.activeStatus) { - ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention - ExchangeStatus.Failed -> TangemTheme.colors.icon.warning - else -> null - } - Notification( - config = it.config, - iconTint = tint, + if (config.notification != null) { + Notification(state = config.notification, activeStatus = config.activeStatus) + } + SpacerH24() + } +} + +@Composable +private fun Notification(state: ExchangeStatusNotifications, activeStatus: ExchangeStatus?) { + AnimatedContent( + targetState = state, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + label = "Exchange Status Notification Change", + ) { notification -> + when (notification) { + is ExchangeStatusNotifications.CommonNotification -> { + com.tangem.core.ui.components.notifications.Notification( + config = notification.config, + iconTint = when (activeStatus) { + ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention + ExchangeStatus.Failed -> TangemTheme.colors.icon.warning + else -> null + }, + containerColor = TangemTheme.colors.background.action, + ) + } + is ExchangeStatusNotifications.TokenRefunded -> { + CurrencyNotification( + config = notification.config, containerColor = TangemTheme.colors.background.action, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), ) } } - SpacerH24() } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index fb15bd476d..fc2733a1ba 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -4,6 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.swaptx.ExchangeAnalyticsStatus import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent @@ -12,6 +13,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel @@ -38,6 +40,7 @@ internal class ExchangeStatusFactory( private val swapRepository: SwapRepository, private val quotesRepository: QuotesRepository, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val swapTransactionStatusStore: SwapTransactionStatusStore, private val dispatchers: CoroutineDispatcherProvider, private val clickIntents: TokenDetailsClickIntents, @@ -81,12 +84,13 @@ internal class ExchangeStatusFactory( } } - suspend fun removeTransactionOnBottomSheetClosed(): TokenDetailsState { + suspend fun removeTransactionOnBottomSheetClosed(isForceTerminal: Boolean = false): TokenDetailsState { val state = currentStateProvider() val bottomSheetConfig = state.bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig ?: return state val selectedTx = bottomSheetConfig.value - return if (selectedTx.activeStatus.isTerminal()) { + val shouldTerminate = selectedTx.activeStatus.isTerminal(selectedTx.isRefundTerminalStatus) || isForceTerminal + return if (shouldTerminate) { swapTransactionRepository.removeTransaction( userWalletId = userWalletId, fromCryptoCurrency = selectedTx.fromCryptoCurrency, @@ -105,12 +109,20 @@ internal class ExchangeStatusFactory( suspend fun updateSwapTxStatuses(swapTxList: PersistentList) = withContext(dispatchers.io) { swapTxList.map { tx -> async { - if (tx.activeStatus.isTerminal()) { + val statusModel = getExchangeStatus(tx.txId) + val isRefundTerminalStatus = statusModel?.refundNetwork == null && + statusModel?.refundContractAddress == null && + tx.provider.type != ExchangeProviderType.DEX_BRIDGE + if (tx.activeStatus.isTerminal(isRefundTerminalStatus)) { tx } else { - val statusModel = getExchangeStatus(tx.txId) - swapTransactionsStateConverter - .updateTxStatus(tx, statusModel) + val addedRefundToken = addRefundCurrencyIfNeeded(statusModel, tx.provider.type) + swapTransactionsStateConverter.updateTxStatus( + tx = tx, + statusModel = statusModel, + refundToken = addedRefundToken, + isRefundTerminalStatus = isRefundTerminalStatus, + ) } } } @@ -143,6 +155,27 @@ internal class ExchangeStatusFactory( } } + /** + * For now do it only for dex-bridge provider + */ + private suspend fun addRefundCurrencyIfNeeded( + status: ExchangeStatusModel?, + type: ExchangeProviderType, + ): CryptoCurrency? { + status ?: return null + if (type != ExchangeProviderType.DEX_BRIDGE) return null + val refundNetwork = status.refundNetwork + val refundContractAddress = status.refundContractAddress + if (refundNetwork != null && refundContractAddress != null) { + return addCryptoCurrenciesUseCase( + userWalletId = userWalletId, + contractAddress = refundContractAddress, + networkId = refundNetwork, + ).getOrNull() + } + return null + } + private fun getExchangeStatusState( savedTransactions: List?, quotes: Set, @@ -157,23 +190,31 @@ internal class ExchangeStatusFactory( ) } - private fun ExchangeStatus?.isTerminal() = - this == ExchangeStatus.Refunded || this == ExchangeStatus.Finished || this == ExchangeStatus.Cancelled + private fun ExchangeStatus?.isTerminal(isRefundTerminal: Boolean): Boolean { + val needTerminalRefund = this == ExchangeStatus.Refunded && isRefundTerminal + return needTerminalRefund || + this == ExchangeStatus.Finished || + this == ExchangeStatus.Cancelled || + this == ExchangeStatus.TxFailed || + this == ExchangeStatus.Unknown + } private fun toAnalyticStatus(status: ExchangeStatus?): ExchangeAnalyticsStatus? { return when (status) { ExchangeStatus.New, ExchangeStatus.Waiting, - ExchangeStatus.WaitingTxHash, ExchangeStatus.Sending, ExchangeStatus.Confirming, ExchangeStatus.Exchanging, -> ExchangeAnalyticsStatus.InProgress + ExchangeStatus.WaitingTxHash -> ExchangeAnalyticsStatus.WaitingTxHash ExchangeStatus.Verifying -> ExchangeAnalyticsStatus.KYC ExchangeStatus.Failed -> ExchangeAnalyticsStatus.Fail + ExchangeStatus.TxFailed -> ExchangeAnalyticsStatus.FailTx ExchangeStatus.Finished -> ExchangeAnalyticsStatus.Done ExchangeStatus.Refunded -> ExchangeAnalyticsStatus.Refunded ExchangeStatus.Cancelled -> ExchangeAnalyticsStatus.Cancelled + ExchangeStatus.Unknown -> ExchangeAnalyticsStatus.Unknown else -> null } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 9332738ab0..833a3a3f0b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -62,4 +62,8 @@ interface TokenDetailsClickIntents { fun onStakeBannerClick() fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) + + fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) + + fun onOpenUrlClick(url: String) } \ No newline at end of file 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 a7877f7ceb..718cb21cf6 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 @@ -103,6 +103,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase, @@ -171,6 +172,7 @@ internal class TokenDetailsViewModel @Inject constructor( swapRepository = swapRepository, quotesRepository = quotesRepository, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase, swapTransactionStatusStore = swapTransactionStatusStore, dispatchers = dispatchers, clickIntents = this, @@ -731,7 +733,8 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onDismissBottomSheet() { - if (internalUiState.value.bottomSheetConfig?.content is ExchangeStatusBottomSheetConfig) { + val bsContent = internalUiState.value.bottomSheetConfig?.content + if (bsContent is ExchangeStatusBottomSheetConfig) { viewModelScope.launch(dispatchers.main) { internalUiState.value = exchangeStatusFactory.removeTransactionOnBottomSheetClosed() } @@ -753,6 +756,20 @@ internal class TokenDetailsViewModel @Inject constructor( router.openUrl(url) } + override fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) { + if (internalUiState.value.bottomSheetConfig?.content is ExchangeStatusBottomSheetConfig) { + viewModelScope.launch { + internalUiState.value = exchangeStatusFactory.removeTransactionOnBottomSheetClosed(true) + } + } + internalUiState.value = stateFactory.getStateWithClosedBottomSheet() + router.openTokenDetails(userWalletId, cryptoCurrency) + } + + override fun onOpenUrlClick(url: String) { + router.openUrl(url) + } + override fun onSwapPromoDismiss() { viewModelScope.launch(dispatchers.main) { shouldShowSwapPromoTokenUseCase.neverToShow() diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index 8c61fc867a..14885a740b 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -21,11 +21,13 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.featuretoggles) implementation(projects.core.navigation) + implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.common.routing) /* Project - Domain */ implementation(projects.domain.legacy) + implementation(projects.domain.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) @@ -50,4 +52,5 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) implementation(deps.timber) + implementation(deps.reKotlin) } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt new file mode 100644 index 0000000000..9a655d973a --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.walletsettings.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +internal sealed class Settings( + category: String = "Settings", + event: String, + params: Map = mapOf(), + error: Throwable? = null, +) : AnalyticsEvent(category, event, params, error) { + + class ButtonCreateBackup : Settings(event = "Button - Create Backup") +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt index 15ab597e72..902d26f1ef 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt @@ -34,8 +34,8 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( private val stateFlow: MutableStateFlow = MutableStateFlow( value = RenameWalletUM( walletNameValue = TextFieldValue(text = params.currentName), - isNameCorrect = false, updateValue = ::updateValue, + isConfirmEnabled = false, onConfirm = { renameWallet(params.userWalletId) }, ), ) @@ -58,12 +58,14 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( stateFlow.update { it.copy( walletNameValue = value, - isNameCorrect = value.text.isNotBlank() && value.text != currentWalletName, + isConfirmEnabled = value.text.isNotBlank() && value.text != currentWalletName, ) } } private fun renameWallet(userWalletId: UserWalletId) = componentScope.launch { + stateFlow.update { it.copy(isConfirmEnabled = false) } + val newName = stateFlow.value.walletNameValue val maybeError = renameWalletUseCase(userWalletId, newName.text).leftOrNull() @@ -71,9 +73,7 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( Timber.e("Unable to rename wallet: $maybeError") val message = when (maybeError) { - is UpdateWalletError.DataError -> resourceReference( - id = R.string.common_unknown_error, - ) + is UpdateWalletError.DataError -> resourceReference(id = R.string.common_unknown_error) is UpdateWalletError.NameAlreadyExists -> resourceReference( id = R.string.user_wallet_list_rename_popup_error_already_exists, formatArgs = wrappedList(newName), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt index ff42b59689..fbcb3d6389 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt @@ -10,7 +10,7 @@ internal class PreviewRenameWalletComponent : RenameWalletComponent { private val previewState = RenameWalletUM( walletNameValue = TextFieldValue(text = "My Wallet"), - isNameCorrect = false, + isConfirmEnabled = false, updateValue = {}, onConfirm = {}, ) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 7649ef9bd7..4dba365556 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -19,8 +19,10 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { userWalletId = UserWalletId("011"), userWalletName = "My Wallet", isReferralAvailable = true, + isLinkMoreCardsAvailable = true, renameWallet = {}, forgetWallet = {}, + onLinkMoreCardsClick = {}, ), ) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt index 4dd5b8c7a7..730cc1ee60 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.text.input.TextFieldValue @Immutable internal data class RenameWalletUM( val walletNameValue: TextFieldValue, - val isNameCorrect: Boolean, val updateValue: (value: TextFieldValue) -> Unit, + val isConfirmEnabled: Boolean, val onConfirm: () -> Unit, ) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index e066301748..a9e5b42932 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -5,6 +5,8 @@ import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.utils.AnalyticsContextProxy import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -16,9 +18,14 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.ContentMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.LegacyAction +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.entity.DialogConfig import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM @@ -43,6 +50,9 @@ internal class WalletSettingsModel @Inject constructor( private val deleteWalletUseCase: DeleteWalletUseCase, private val itemsBuilder: ItemsBuilder, override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val analyticsContextProxy: AnalyticsContextProxy, + private val reduxStateHolder: ReduxStateHolder, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -75,6 +85,7 @@ internal class WalletSettingsModel @Inject constructor( userWalletId = userWallet.walletId, userWalletName = userWallet.name, isReferralAvailable = userWallet.cardTypesResolver.isTangemWallet(), + isLinkMoreCardsAvailable = userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, forgetWallet = { messageSender.send( @@ -98,6 +109,9 @@ internal class WalletSettingsModel @Inject constructor( }, ) }, + onLinkMoreCardsClick = { + onLinkMoreCardsClick(scanResponse = userWallet.scanResponse) + }, ) private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation) { @@ -126,4 +140,19 @@ internal class WalletSettingsModel @Inject constructor( router.replaceAll(AppRoute.Home) } } + + private fun onLinkMoreCardsClick(scanResponse: ScanResponse) { + analyticsEventHandler.send(Settings.ButtonCreateBackup()) + + analyticsContextProxy.addContext(scanResponse) + + reduxStateHolder.dispatch( + LegacyAction.StartOnboardingProcess( + scanResponse = scanResponse, + canSkipBackup = false, + ), + ) + + router.push(AppRoute.OnboardingWallet()) + } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt index fa02de24c9..5ff04fe2e7 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt @@ -23,7 +23,7 @@ internal fun RenameWalletDialog(model: RenameWalletUM, onDismiss: () -> Unit) { fieldValue = value, confirmButton = DialogButton( title = stringResource(id = R.string.common_ok), - enabled = model.isNameCorrect, + enabled = model.isConfirmEnabled, onClick = model.onConfirm, ), dismissButton = DialogButton( diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index deb049021a..941f1c53f2 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -19,15 +19,18 @@ internal class ItemsBuilder @Inject constructor( private val router: Router, ) { + @Suppress("LongParameterList") fun buildItems( userWalletId: UserWalletId, userWalletName: String, + isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, forgetWallet: () -> Unit, renameWallet: () -> Unit, + onLinkMoreCardsClick: () -> Unit, ): PersistentList = persistentListOf( buildNameItem(userWalletName, renameWallet), - buildCardItem(userWalletId, isReferralAvailable), + buildCardItem(userWalletId, isLinkMoreCardsAvailable, isReferralAvailable, onLinkMoreCardsClick), buildForgetItem(forgetWallet), ) @@ -38,26 +41,38 @@ internal class ItemsBuilder @Inject constructor( onClick = renameWallet, ) - private fun buildCardItem(userWalletId: UserWalletId, isReferralAvailable: Boolean) = - WalletSettingsItemUM.WithItems( - id = "card", - description = resourceReference(R.string.settings_card_settings_footer), - blocks = buildList { + private fun buildCardItem( + userWalletId: UserWalletId, + isLinkMoreCardsAvailable: Boolean, + isReferralAvailable: Boolean, + onLinkMoreCardsClick: () -> Unit, + ) = WalletSettingsItemUM.WithItems( + id = "card", + description = resourceReference(R.string.settings_card_settings_footer), + blocks = buildList { + if (isLinkMoreCardsAvailable) { BlockUM( - text = resourceReference(R.string.card_settings_title), - iconRes = R.drawable.ic_card_settings_24, - onClick = { router.push(AppRoute.CardSettings(userWalletId)) }, + text = resourceReference(R.string.details_row_title_create_backup), + iconRes = R.drawable.ic_more_cards_24, + onClick = onLinkMoreCardsClick, ).let(::add) + } - if (isReferralAvailable) { - BlockUM( - text = resourceReference(R.string.referral_title), - iconRes = R.drawable.ic_add_friends_24, - onClick = { router.push(AppRoute.ReferralProgram(userWalletId)) }, - ).let(::add) - } - }.toImmutableList(), - ) + BlockUM( + text = resourceReference(R.string.card_settings_title), + iconRes = R.drawable.ic_card_settings_24, + onClick = { router.push(AppRoute.CardSettings(userWalletId)) }, + ).let(::add) + + if (isReferralAvailable) { + BlockUM( + text = resourceReference(R.string.details_referral_title), + iconRes = R.drawable.ic_add_friends_24, + onClick = { router.push(AppRoute.ReferralProgram(userWalletId)) }, + ).let(::add) + } + }.toImmutableList(), + ) private fun buildForgetItem(forgetWallet: () -> Unit) = WalletSettingsItemUM.WithItems( id = "forget", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt index c659d29d7c..6fd16546b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt @@ -3,11 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent data class PushNotificationsBottomSheetConfig( - val isFirstTimeRequested: Boolean, - val wasInitiallyAsk: Boolean, val onRequest: () -> Unit, - val onRequestLater: () -> Unit, + val onNeverRequest: () -> Unit, val onAllow: () -> Unit, val onDeny: () -> Unit, - val openSettings: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt index dcc56be741..aeaad6ef3f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt @@ -39,7 +39,6 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC val isClicked = remember { mutableStateOf(false) } val requestPushPermission = requestPushPermission( pushPermission = getPushPermissionOrNull(), - isFirstTimeAsking = content.isFirstTimeRequested, isClicked = isClicked, onAllow = { content.onAllow() @@ -49,7 +48,6 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC content.onDeny() onDismiss() }, - onOpenSettings = content.openSettings, ) Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) { @@ -78,13 +76,9 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC ), ) { SecondaryButton( - text = if (content.wasInitiallyAsk) { - stringResource(R.string.common_later) - } else { - stringResource(R.string.common_cancel) - }, + text = stringResource(R.string.common_cancel), onClick = { - content.onRequestLater() + content.onNeverRequest() onDismiss() }, modifier = Modifier.weight(1f), @@ -110,13 +104,10 @@ private fun PushNotificationsSheetContent_Preview() { TangemThemePreview { PushNotificationsSheetContent( PushNotificationsBottomSheetConfig( - isFirstTimeRequested = false, - wasInitiallyAsk = false, onRequest = {}, - onRequestLater = {}, + onNeverRequest = {}, onAllow = {}, onDeny = {}, - openSettings = {}, ), onDismiss = {}, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index f4ac9a87e3..b38bc57b61 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.navigation.settings.SettingsManager import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase @@ -65,11 +64,8 @@ internal class WalletViewModel @Inject constructor( private val walletDeepLinksHandler: WalletDeepLinksHandler, private val walletNameMigrationUseCase: WalletNameMigrationUseCase, private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, - private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase, - private val isFirstTimeAskingPermissionUseCase: IsFirstTimeAskingPermissionUseCase, private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles, - private val settingsManager: SettingsManager, analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { @@ -159,24 +155,14 @@ internal class WalletViewModel @Inject constructor( delay(timeMillis = 1_800) - val isFirstTimeRequested = isFirstTimeAskingPermissionUseCase(PUSH_PERMISSION).getOrElse { true } - val wasInitiallyAsk = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { true } - val onRequestLater: () -> Unit = if (wasInitiallyAsk) { - clickIntents::onDelayAskPushPermission - } else { - clickIntents::onNeverAskPushPermission - } stateHolder.showBottomSheet( content = PushNotificationsBottomSheetConfig( - isFirstTimeRequested = isFirstTimeRequested, - wasInitiallyAsk = wasInitiallyAsk, onRequest = clickIntents::onRequestPushPermission, - onRequestLater = onRequestLater, + onNeverRequest = { clickIntents.onNeverAskPushPermission(false) }, onAllow = clickIntents::onAllowPushPermission, onDeny = clickIntents::onDenyPushPermission, - openSettings = settingsManager::openSettings, ), - onDismiss = onRequestLater, + onDismiss = { clickIntents.onNeverAskPushPermission(true) }, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt index fe8848daf5..13a301f372 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt @@ -2,9 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.settings.DelayPermissionRequestUseCase import com.tangem.domain.settings.NeverRequestPermissionUseCase -import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import dagger.hilt.android.scopes.ViewModelScoped @@ -15,9 +13,7 @@ internal interface WalletPushPermissionClickIntents { fun onRequestPushPermission() - fun onDelayAskPushPermission() - - fun onNeverAskPushPermission() + fun onNeverAskPushPermission(isUserDismissed: Boolean) fun onDenyPushPermission() @@ -26,33 +22,23 @@ internal interface WalletPushPermissionClickIntents { @ViewModelScoped internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( - private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase, private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, - private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase, private val analyticsEventHandler: AnalyticsEventHandler, ) : BaseWalletClickIntents(), WalletPushPermissionClickIntents { + private var isUserDismissedDialog: Boolean = true override fun onRequestPushPermission() { + isUserDismissedDialog = false analyticsEventHandler.send( PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Main), ) - viewModelScope.launch { - setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) - } } - override fun onDelayAskPushPermission() { + override fun onNeverAskPushPermission(isUserDismissed: Boolean) { + if (!isUserDismissedDialog) return + isUserDismissedDialog = isUserDismissed viewModelScope.launch { - analyticsEventHandler.send( - PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Main), - ) - delayPermissionRequestUseCase(PUSH_PERMISSION) - } - } - - override fun onNeverAskPushPermission() { - viewModelScope.launch { - analyticsEventHandler.send(PushNotificationAnalyticEvents.ButtonCancel) + PushNotificationAnalyticEvents.ButtonCancel(AnalyticsParam.ScreensSources.Main) neverRequestPermissionUseCase(PUSH_PERMISSION) } } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 7485afe234..740b7c1570 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-724" +tangemBlockchainSdk = "develop-728" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index f114465769..8bdae098a7 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -1,7 +1,8 @@ package com.tangem.lib.crypto +import com.tangem.blockchain.common.Amount import com.tangem.lib.crypto.models.* -import java.math.BigDecimal +import java.math.BigInteger interface TransactionManager { @@ -21,7 +22,7 @@ interface TransactionManager { @Throws(IllegalStateException::class) suspend fun getFee( networkId: String, - amountToSend: BigDecimal, + amountToSend: Amount, currencyToSend: Currency, destinationAddress: String, increaseBy: Int?, @@ -29,6 +30,9 @@ interface TransactionManager { derivationPath: String?, ): ProxyFees + @Throws(IllegalStateException::class) + suspend fun getFeeForGas(networkId: String, gas: BigInteger, derivationPath: String?): ProxyFees + @Throws(IllegalStateException::class) suspend fun updateWalletManager(networkId: String, derivationPath: String?) diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/errors/UserCancelledException.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/errors/UserCancelledException.kt deleted file mode 100644 index bdaf360494..0000000000 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/errors/UserCancelledException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.lib.crypto.models.errors - -class UserCancelledException : Exception() \ No newline at end of file diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/BigIntegerExt.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/BigIntegerExt.kt index 4b129ed3c4..620ff5a6c2 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/BigIntegerExt.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/BigIntegerExt.kt @@ -5,7 +5,7 @@ import java.math.BigDecimal import java.math.BigInteger internal fun BigInteger.toBigDecimal(decimals: Int): BigDecimal { - return BigDecimal(this).movePointLeft(decimals) + return this.toBigDecimal().movePointLeft(decimals) } internal fun BigInteger.toInstant(): Instant {