diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 4434311a95..d34f20eb74 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -8,6 +8,8 @@ import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -69,4 +71,10 @@ internal object CardDomainModule { ): GetExtendedPublicKeyForCurrencyUseCase { return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository) } + + @Provides + @ViewModelScoped + fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase { + return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager) + } } \ 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 da46031149..ecb92a6ac9 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 @@ -7,7 +7,9 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.settings.* import com.tangem.domain.settings.repositories.AppRatingRepository +import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.tap.domain.TangemSdkManager import com.tangem.domain.settings.repositories.SwapPromoRepository import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository @@ -108,17 +110,25 @@ internal object SettingsDomainModule { @Provides @ViewModelScoped fun provideShouldShowSwapPromoWalletUseCase( - swapPromoRepository: SwapPromoRepository, + promoSettingsRepository: PromoSettingsRepository, ): ShouldShowSwapPromoWalletUseCase { - return ShouldShowSwapPromoWalletUseCase(swapPromoRepository) + return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository) + } + + @Provides + @ViewModelScoped + fun provideShouldShowTravalaPromoWalletUseCase( + promoSettingsRepository: PromoSettingsRepository, + ): ShouldShowTravalaPromoWalletUseCase { + return ShouldShowTravalaPromoWalletUseCase(promoSettingsRepository) } @Provides @ViewModelScoped fun provideShouldShowSwapPromoTokenUseCase( - swapPromoRepository: SwapPromoRepository, + promoSettingsRepository: PromoSettingsRepository, ): ShouldShowSwapPromoTokenUseCase { - return ShouldShowSwapPromoTokenUseCase(swapPromoRepository) + return ShouldShowSwapPromoTokenUseCase(promoSettingsRepository) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 6821d4a992..91c7d4aadb 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -346,4 +346,12 @@ internal object TokensDomainModule { ): RunPolkadotAccountHealthCheckUseCase { return RunPolkadotAccountHealthCheckUseCase(repository) } + + @Provides + @ViewModelScoped + fun provideGetNetworkStatusesUseCase(networksRepository: NetworksRepository): GetNetworkAddressesUseCase { + return GetNetworkAddressesUseCase( + networksRepository = networksRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt new file mode 100644 index 0000000000..b0fadb5bcf --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.domain.card + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.card.DeleteSavedAccessCodesUseCase +import com.tangem.tap.domain.TangemSdkManager + +internal class DefaultDeleteSavedAccessCodesUseCase( + private val tangemSdkManager: TangemSdkManager, +) : DeleteSavedAccessCodesUseCase { + + override suspend fun invoke(cardId: String): Either { + tangemSdkManager.deleteSavedUserCodes(setOf(cardId)) + .doOnFailure { return it.left() } + .doOnSuccess { return Unit.right() } + + return Unit.right() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt index 7e7a781189..3db56b3dcb 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt @@ -8,15 +8,20 @@ internal class DelegatedKeystoreManager( private val keystoreManagerProvider: Provider, ) : KeystoreManager { - override suspend fun get(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String): SecretKey? { - return keystoreManagerProvider().get(masterKeyConfig, keyAlias) + override suspend fun get( + masterKeyConfig: KeystoreManager.MasterKeyConfig, + keyAlias: String, + forceAuthentication: Boolean, + ): SecretKey? { + return keystoreManagerProvider().get(masterKeyConfig, keyAlias, forceAuthentication) } override suspend fun get( masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAliases: Set, + forceAuthentication: Boolean, ): Map { - return keystoreManagerProvider().get(masterKeyConfig, keyAliases) + return keystoreManagerProvider().get(masterKeyConfig, keyAliases, forceAuthentication) } override suspend fun store(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String, key: SecretKey) { diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 6c4c24f485..6840975e44 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -14,6 +14,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase +import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -39,6 +40,7 @@ internal class MainViewModel @Inject constructor( private val blockchainSDKFactory: BlockchainSDKFactory, private val userWalletsListManager: UserWalletsListManager, private val walletManagersFacade: WalletManagersFacade, + private val sendFeatureToggles: SendFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -61,6 +63,7 @@ internal class MainViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() } updateAppCurrencies() + updateSendFeatureToggle() observeFlips() displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() @@ -108,6 +111,12 @@ internal class MainViewModel @Inject constructor( } } + private fun updateSendFeatureToggle() { + viewModelScope.launch(dispatchers.main) { + sendFeatureToggles.fetchNewSendEnabled() + } + } + private fun observeFlips() { listenToFlipsUseCase().launchIn(viewModelScope) } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index eb8ef3b54b..b77078a426 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -101,6 +101,7 @@ sealed class AnalyticsParam { data object Min : FeeType("Min") data object Normal : FeeType("Normal") data object Max : FeeType("Max") + data object Custom : FeeType("Custom") companion object { fun fromString(feeType: String): FeeType { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromotionInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromotionInfoResponse.kt index fa9140eea1..668a77c321 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromotionInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/promotion/models/PromotionInfoResponse.kt @@ -13,6 +13,7 @@ data class PromotionInfoResponse( data class BannerState( @Json(name = "timeline") val timeline: Timeline, @Json(name = "status") val status: String, + @Json(name = "link") val link: String?, ) @JsonClass(generateAdapter = true) 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 0a5dcf3fd9..2f5e52b496 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 @@ -126,4 +126,7 @@ interface TangemTechApi { @Header("card_id") cardId: String, @Path("account_id") accountId: Int, ): ApiResponse + + @GET("features") + suspend fun getFeatures(): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/FeaturesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/FeaturesResponse.kt new file mode 100644 index 0000000000..2d72021cd0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/FeaturesResponse.kt @@ -0,0 +1,7 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +data class FeaturesResponse( + @Json(name = "send") val isNewSendEnabled: Boolean, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 4dfd9d6966..cfafab3c05 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 @@ -61,6 +61,10 @@ object PreferencesKeys { booleanPreferencesKey(name = "isTokenSwapPromoChangellyShown") } + val IS_WALLET_TRAVALA_PROMO_SHOWN_KEY by lazy { + booleanPreferencesKey(name = "isWalletTravalaPromoShown") + } + val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") } val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") } diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index d3be4f5db5..d91a3988e8 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -9,7 +9,7 @@ }, { "name": "REDESIGNED_SEND_SCREEN_ENABLED", - "version": "5.9.1" + "version": "5.10.0" }, { "name": "LOCAL_USER_LOGS_ENABLED", diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 961dc949c5..e7c5bfc4e7 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -46,4 +46,5 @@ dependencies { implementation(deps.zxing.qrCore) implementation(deps.jodatime) implementation(deps.timber) + implementation(deps.markdown) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index 73a58d56a1..d39c09ecce 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -95,10 +95,14 @@ fun AmountTextField( SimpleTextField( value = value, onValueChange = { newText -> - if (decimalFormat.isValidSymbols(newText)) { - val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals) - onValueChange(trimmed) - } + onValueChange( + prepareEnter( + oldValue = value, + newValue = newText, + decimalFormat = decimalFormat, + decimals = decimals, + ), + ) }, textStyle = textStyle.copy( fontSize = fontSize, @@ -117,8 +121,37 @@ fun AmountTextField( } } +private fun prepareEnter(oldValue: String, newValue: String, decimalFormat: DecimalFormat, decimals: Int): String { + val decimalSymbol = decimalFormat.decimalFormatSymbols.decimalSeparator + return if (decimalFormat.isValidSymbols(newValue)) { + val parsedValue = newValue.parseBigDecimalOrNull()?.toPlainString() + ?: if (newValue.isBlank()) "" else oldValue + val replacedWithSymbol = if (parsedValue.findLast { it != decimalSymbol } != null) { + when { + parsedValue.findLast { it == COMMA_SEPARATOR } != null -> { + parsedValue.replace(COMMA_SEPARATOR, decimalSymbol) + } + parsedValue.findLast { it == POINT_SEPARATOR } != null -> { + parsedValue.replace(POINT_SEPARATOR, decimalSymbol) + } + else -> parsedValue + } + } else { + parsedValue + } + val joinedSymbol = if (newValue.endsWith(COMMA_SEPARATOR) || newValue.endsWith(POINT_SEPARATOR)) { + replacedWithSymbol.plus(decimalSymbol) + } else { + replacedWithSymbol + } + decimalFormat.getValidatedNumberWithFixedDecimals(joinedSymbol, decimals) + } else { + oldValue + } +} + private fun DecimalFormat.isValidSymbols(text: String): Boolean { - return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text) + return checkDecimalSeparatorDuplicate(text) } // region preview diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index ff2bf968ae..59128a93a9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -38,6 +38,7 @@ fun SimpleTextField( keyboardActions: KeyboardActions = KeyboardActions.Default, color: Color = TangemTheme.colors.text.primary1, textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), + placeholderColor: Color = TangemTheme.colors.text.disabled, readOnly: Boolean = false, isValuePasted: Boolean = false, onValuePastedTriggerDismiss: () -> Unit = {}, @@ -108,6 +109,7 @@ fun SimpleTextField( value = value, textStyle = textStyle, textValue = textValue, + color = placeholderColor, ) }, modifier = modifier @@ -122,6 +124,7 @@ private fun SimpleTextPlaceholder( value: String, textStyle: TextStyle, textValue: @Composable () -> Unit, + color: Color = TangemTheme.colors.text.disabled, ) { Box { if (value.isBlank() && placeholder != null) { @@ -132,7 +135,7 @@ private fun SimpleTextPlaceholder( Text( text = it.resolveReference(), style = textStyle, - color = TangemTheme.colors.text.disabled, + color = color, ) } } 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 6d339e48dd..c572bb3f83 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 @@ -51,12 +51,14 @@ fun Notification( modifier: Modifier = Modifier, containerColor: Color? = null, iconTint: Color? = null, + isEnabled: Boolean = true, ) { BaseContainer( buttonsState = config.buttonsState, onClick = config.onClick, modifier = modifier, containerColor = containerColor, + isEnabled = isEnabled, ) { Column( modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), @@ -67,15 +69,16 @@ fun Notification( iconTint = iconTint, title = config.title, subtitle = config.subtitle, - isClickableComponent = config.onClick != null, + isClickableComponent = isEnabled && config.onClick != null, ) - Buttons(state = config.buttonsState) + Buttons(state = config.buttonsState, isEnabled = isEnabled) } CloseableIconButton( onClick = config.onCloseClick, modifier = Modifier.align(alignment = Alignment.TopEnd), + isEnabled = isEnabled, ) } } @@ -85,6 +88,7 @@ private fun BaseContainer( buttonsState: NotificationConfig.ButtonsState?, onClick: (() -> Unit)?, modifier: Modifier = Modifier, + isEnabled: Boolean = true, containerColor: Color? = null, content: @Composable BoxScope.() -> Unit, ) { @@ -101,7 +105,7 @@ private fun BaseContainer( modifier = modifier .defaultMinSize(minHeight = TangemTheme.dimens.size62) .fillMaxWidth(), - enabled = onClick != null, + enabled = onClick != null && isEnabled, shape = TangemTheme.shapes.roundedCornersXMedium, color = containerColor ?: tempContainerColor, ) { @@ -179,27 +183,31 @@ private fun TextsBlock(title: TextReference, subtitle: TextReference) { } @Composable -private fun Buttons(state: NotificationButtonsState?) { +private fun Buttons(state: NotificationButtonsState?, isEnabled: Boolean = true) { when (state) { - is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton(config = state) - is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state) - is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state) + is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton( + config = state, + isEnabled = isEnabled, + ) + is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state, isEnabled = isEnabled) + is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state, isEnabled = isEnabled) null -> Unit } } @Composable -private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig) { +private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig, isEnabled: Boolean = true) { SecondaryButton( text = config.text.resolveReference(), onClick = config.onClick, modifier = Modifier.fillMaxWidth(), size = TangemButtonSize.WideAction, + enabled = isEnabled, ) } @Composable -private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig) { +private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig, isEnabled: Boolean = true) { if (config.iconResId != null) { PrimaryButtonIconEnd( text = config.text.resolveReference(), @@ -207,6 +215,7 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo onClick = config.onClick, modifier = Modifier.fillMaxWidth(), size = TangemButtonSize.WideAction, + enabled = isEnabled, ) } else { PrimaryButton( @@ -214,18 +223,20 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo onClick = config.onClick, modifier = Modifier.fillMaxWidth(), size = TangemButtonSize.WideAction, + enabled = isEnabled, ) } } @Composable -private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) { +private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig, isEnabled: Boolean = true) { Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8)) { SecondaryButton( text = config.secondaryText.resolveReference(), onClick = config.onSecondaryClick, modifier = Modifier.weight(weight = 1f), size = TangemButtonSize.WideAction, + enabled = isEnabled, ) PrimaryButton( @@ -233,12 +244,13 @@ private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) { onClick = config.onPrimaryClick, modifier = Modifier.weight(weight = 1f), size = TangemButtonSize.WideAction, + enabled = isEnabled, ) } } @Composable -private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { +private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier, isEnabled: Boolean = true) { AnimatedVisibility(visible = onClick != null, modifier = modifier) { onClick ?: return@AnimatedVisibility @@ -255,6 +267,7 @@ private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Mod interactionSource = remember { MutableInteractionSource() }, indication = LocalIndication.current, role = Role.Button, + enabled = isEnabled, onClick = onClick, ), ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index 00945dbb44..076c60da41 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -49,7 +49,11 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = modifier = modifier .defaultMinSize(minHeight = TangemTheme.dimens.size62) .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium), + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable( + enabled = config.onClick != null, + onClick = config.onClick ?: {}, + ), ) { val (iconRef, titleRef, subtitleRef, closeIconRef, buttonRef, backgroundRef) = createRefs() diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt new file mode 100644 index 0000000000..9e6ba3db1d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt @@ -0,0 +1,215 @@ +package com.tangem.core.ui.components.notifications + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.ScaleFactor +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.LineBreak +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonColors +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemColorPalette.White +import com.tangem.core.ui.res.TangemTheme + +/** + * Travala notification with image background + * @see Travala Promo + */ +@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries") +@Composable +fun TravalaNotificationWithBackground(config: NotificationConfig, modifier: Modifier = Modifier) { + val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig + + Box( + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size62) + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(Color.Black) + .clickable( + enabled = config.onClick != null, + onClick = config.onClick ?: {}, + ), + propagateMinConstraints = true, + contentAlignment = Alignment.TopStart, + ) { + val density = LocalDensity.current + Image( + painter = painterResource(R.drawable.img_travala_banner_promo_background), + contentDescription = null, + contentScale = TravalaBackgroundScale(density), + alignment = Alignment.TopStart, + modifier = Modifier + .matchParentSize() + .wrapContentSize(unbounded = true, align = Alignment.TopStart) + .align(Alignment.TopStart), + ) + Image( + painter = painterResource(R.drawable.img_travala_banner_promo_background_2), + contentDescription = null, + contentScale = TravalaBackgroundScale(density), + alignment = Alignment.TopStart, + modifier = Modifier + .matchParentSize() + .wrapContentSize(unbounded = true, align = Alignment.TopEnd) + .align(Alignment.TopEnd), + ) + Column { + Row { + Box(modifier = Modifier.size(87.dp)) + Column( + Modifier + .weight(1f) + .padding(top = TangemTheme.dimens.spacing12), + ) { + Text( + text = config.title.resolveReference(), + style = TangemTheme.typography.button.copy( + lineBreak = LineBreak.Heading, + ), + color = TangemTheme.colors.text.constantWhite, + ) + SpacerH8() + Text( + text = formatSubtitle(config.subtitle.resolveReference()), + style = TangemTheme.typography.caption2.copy( + lineBreak = LineBreak.Heading, + ), + color = TangemTheme.colors.text.constantWhite, + ) + } + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors.text.constantWhite, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing2, + ) + .size(TangemTheme.dimens.size16) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { + config.onCloseClick?.invoke() + }, + ) + } + + TangemButton( + text = button?.text?.resolveReference().orEmpty(), + icon = TangemButtonIconPosition.None, + onClick = button?.onClick ?: {}, + colors = TangemButtonColors( + backgroundColor = White.copy(alpha = 0.3f), + contentColor = White, + disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContentColor = TangemTheme.colors.text.disabled, + ), + enabled = true, + showProgress = false, + modifier = Modifier + .padding(TangemTheme.dimens.spacing12) + .fillMaxWidth(), + ) + } + } +} + +private const val TRAVALA_BACKGROUND_SRC_IMG_SCALE = 4 + +private class TravalaBackgroundScale( + val density: Density, +) : ContentScale { + override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor { + with(density) { + val originalWidth = (srcSize.width / TRAVALA_BACKGROUND_SRC_IMG_SCALE).dp.toPx() + val widthScale = originalWidth / srcSize.width + val originalHeight = (srcSize.height / TRAVALA_BACKGROUND_SRC_IMG_SCALE).dp.toPx() + val heightScale = originalHeight / srcSize.height + return ScaleFactor(widthScale, heightScale) + } + } +} + +@Composable +private fun formatSubtitle(subtitle: String): AnnotatedString { + val pattern = Regex("\\*\\*(.*?)\\*\\*") + var startIndex = 0 + val annotatedString = buildAnnotatedString { + pattern.findAll(subtitle).forEach { matchResult -> + val index = matchResult.range.first + val matchedValue = matchResult.groups[1]?.value ?: "" + + // appends unformatted part + append(subtitle.substring(startIndex, index)) + + // applies style on ^^-wrapped parts + withStyle(SpanStyle(fontWeight = TangemTheme.typography.caption1.fontWeight)) { + append(matchedValue) + } + + // goes to next part + startIndex = matchResult.range.last + 1 + } + + // appends remaining ending if exists + append(subtitle.substring(startIndex)) + } + + return annotatedString +} + +//region preview +@Preview +@Composable +private fun TravalaNotificationWithBackgroundPreview() { + TangemTheme { + TravalaNotificationWithBackground( + config = NotificationConfig( + title = resourceReference( + id = R.string.main_travala_promotion_title, + ), + subtitle = resourceReference( + id = R.string.main_travala_promotion_description, + formatArgs = wrappedList("May 13", "June 12"), + ), + iconResId = R.drawable.img_swap_promo, + backgroundResId = R.drawable.img_travala_banner_promo_background, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(id = R.string.token_swap_promotion_button), + onClick = {}, + ), + ), + ) + } +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt new file mode 100644 index 0000000000..408e9130ee --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt @@ -0,0 +1,56 @@ +package com.tangem.core.ui.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.ast.ASTNode +import org.intellij.markdown.ast.getTextInNode +import org.intellij.markdown.flavours.commonmark.CommonMarkFlavourDescriptor +import org.intellij.markdown.parser.MarkdownParser + +/** Markdown parser */ +@Composable +fun rememberMarkdownParser() = remember { + MarkdownParser(CommonMarkFlavourDescriptor()) +} + +/** + * Styling markdown tree recursively + * + * @param markdownText original text + * @param node current processed node + */ +@Composable +fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode): AnnotatedString.Builder { + when (node.type) { + MarkdownElementTypes.MARKDOWN_FILE, MarkdownElementTypes.PARAGRAPH -> { + node.children.forEach { childNode -> + appendMarkdown( + markdownText = markdownText, + node = childNode, + ) + } + } + MarkdownElementTypes.STRONG -> { + withStyle(SpanStyle(fontWeight = FontWeight.Medium)) { + node.children + .drop(2) + .dropLast(2) + .forEach { childNode -> + appendMarkdown( + markdownText = markdownText, + node = childNode, + ) + } + } + } + else -> { + append(node.getTextInNode(markdownText).toString()) + } + } + return this +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 766815d328..2f22994f64 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -8,6 +8,9 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import org.intellij.markdown.MarkdownElementTypes /** * Utility class for creating text as [String] or [StringRes]. @@ -160,6 +163,29 @@ fun TextReference.resolveReference(resources: Resources): String { } } +/** Resolve [TextReference] as [AnnotatedString] */ +@Composable +fun TextReference.resolveAnnotatedReference(): AnnotatedString { + return when (this) { + is TextReference.Res -> { + val args = formatArgs + .map { if (it is TextReference) it.resolveReference() else it } + .toTypedArray() + + formatAnnotated(stringResource(id = id, *args)) + } + is TextReference.PluralRes -> formatAnnotated( + pluralStringResource(id, count, *formatArgs.toTypedArray()), + ) + is TextReference.Str -> formatAnnotated(value) + is TextReference.Combined -> buildAnnotatedString { + refs.forEach { + append(formatAnnotated(it.resolveReference())) + } + } + } +} + /** Concatenate [this] reference with [ref] */ operator fun TextReference.plus(ref: TextReference): TextReference { return when (this) { @@ -169,4 +195,14 @@ operator fun TextReference.plus(ref: TextReference): TextReference { is TextReference.Str, -> TextReference.Combined(refs = wrappedList(this, ref)) } +} + +@Composable +private fun formatAnnotated(rawString: String): AnnotatedString { + val markdownDescriptor = rememberMarkdownParser() + val parsedTree = markdownDescriptor.parse(MarkdownElementTypes.MARKDOWN_FILE, rawString, true) + + return buildAnnotatedString { + appendMarkdown(markdownText = rawString, node = parsedTree) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 0ea40469e2..d42121d3f0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.utils +import android.text.format.DateFormat import org.joda.time.DateTime import org.joda.time.format.DateTimeFormat import org.joda.time.format.DateTimeFormatter @@ -55,6 +56,16 @@ object DateTimeFormatters { .withLocale(Locale.getDefault()) } + /** + * In API version < 24, there may be some problems with getting the best date and time format pattern. + */ + val dateMMMMd: DateTimeFormatter by lazy { + DateTimeFormatterBuilder() + .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMM d")) + .toFormatter() + .withLocale(Locale.getDefault()) + } + val dateTimeFormatter: DateTimeFormatter by lazy { DateTimeFormat.forPattern("dd.MM.yyyy HH:mm") } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt index 5006df1d7a..2d685763e3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DecimalFormatterExt.kt @@ -10,9 +10,9 @@ import java.text.DecimalFormatSymbols import java.util.Locale private const val TEXT_CHUNK_THOUSAND = 3 -private const val POINT_SEPARATOR = '.' -private const val COMMA_SEPARATOR = ',' private const val SCIENTIFIC_NOTATION = 'e' +const val POINT_SEPARATOR = '.' +const val COMMA_SEPARATOR = ',' const val DECIMAL_SEPARATOR_LIMIT = 1 @Composable diff --git a/core/ui/src/main/res/drawable/img_travala_banner_promo_background.webp b/core/ui/src/main/res/drawable/img_travala_banner_promo_background.webp new file mode 100644 index 0000000000..bc3845b8b7 Binary files /dev/null and b/core/ui/src/main/res/drawable/img_travala_banner_promo_background.webp differ diff --git a/core/ui/src/main/res/drawable/img_travala_banner_promo_background_2.webp b/core/ui/src/main/res/drawable/img_travala_banner_promo_background_2.webp new file mode 100644 index 0000000000..c051b16418 Binary files /dev/null and b/core/ui/src/main/res/drawable/img_travala_banner_promo_background_2.webp differ 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 3121a961d7..a92a4f5eb9 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 @@ -23,7 +23,17 @@ internal class DefaultPromoRepository( }.getOrNull() } + override suspend fun getTravalaPromoBanner(): PromoBanner? { + return runCatching(dispatchers.io) { + promoResponseConverter.convert( + tangemApi.getPromotionInfo(TRAVALA) + .getOrThrow(), + ) + }.getOrNull() + } + private companion object { private const val CHANGELLY_NAME = "changelly" + private const val TRAVALA = "travala" } } \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt b/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt index c7ad8f331a..151675506b 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/converters/PromoResponseConverter.kt @@ -13,6 +13,7 @@ class PromoResponseConverter : Converter { name = value.name, bannerState = PromoBanner.BannerState( status = bannerState.status, + link = bannerState.link, timeline = PromoBanner.Timeline( start = DateTime.parse(bannerState.timeline.start), end = DateTime.parse(bannerState.timeline.end), diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt similarity index 55% rename from data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt rename to data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt index 6d9ccf740a..80ba44157d 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt @@ -3,36 +3,48 @@ package com.tangem.data.settings import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_TRAVALA_PROMO_SHOWN_KEY import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.store -import com.tangem.domain.settings.repositories.SwapPromoRepository +import com.tangem.domain.settings.repositories.PromoSettingsRepository import kotlinx.coroutines.flow.Flow /** * Repository for showing swap promo notification. */ -class DefaultSwapPromoRepository( +class DefaultPromoSettingsRepository( private val appPreferencesStore: AppPreferencesStore, -) : SwapPromoRepository { - override fun isReadyToShowWalletPromo(): Flow { +) : PromoSettingsRepository { + override fun isReadyToShowWalletSwapPromo(): Flow { return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, true) } - override fun isReadyToShowTokenPromo(): Flow { + override fun isReadyToShowTokenSwapPromo(): Flow { return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY, true) } - override suspend fun setNeverToShowWalletPromo() { + override suspend fun setNeverToShowWalletSwapPromo() { appPreferencesStore.store( key = IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, value = false, ) } - override suspend fun setNeverToShowTokenPromo() { + override suspend fun setNeverToShowTokenSwapPromo() { appPreferencesStore.store( key = IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY, value = false, ) } + + override fun isReadyToShowWalletTravalaPromo(): Flow { + return appPreferencesStore.get(IS_WALLET_TRAVALA_PROMO_SHOWN_KEY, true) + } + + override suspend fun setNeverToShowWalletTravalaPromo() { + appPreferencesStore.store( + key = IS_WALLET_TRAVALA_PROMO_SHOWN_KEY, + value = false, + ) + } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 6211aace4f..a1613d958b 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -2,10 +2,14 @@ package com.tangem.data.settings.di import com.tangem.data.settings.DefaultAppRatingRepository import com.tangem.data.settings.DefaultSettingsRepository +import com.tangem.data.settings.DefaultPromoSettingsRepository +import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.data.settings.DefaultSwapPromoRepository import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.settings.repositories.PromoSettingsRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.domain.settings.repositories.SwapPromoRepository import dagger.Module import dagger.Provides @@ -31,7 +35,7 @@ internal object SettingsDataModule { @Provides @Singleton - fun provideSwapPromoRepository(appPreferencesStore: AppPreferencesStore): SwapPromoRepository { - return DefaultSwapPromoRepository(appPreferencesStore = appPreferencesStore) + fun providePromoSettingsSettingsRepository(appPreferencesStore: AppPreferencesStore): PromoSettingsRepository { + return DefaultPromoSettingsRepository(appPreferencesStore = appPreferencesStore) } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/DeleteSavedAccessCodesUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/DeleteSavedAccessCodesUseCase.kt new file mode 100644 index 0000000000..dc118e8f15 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/DeleteSavedAccessCodesUseCase.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.card + +import arrow.core.Either + +interface DeleteSavedAccessCodesUseCase { + + suspend operator fun invoke(cardId: String): Either +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index 0cbee25c28..56a874bd6a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -30,6 +30,12 @@ interface CardTypesResolver { fun isSatoshiFriendsWallet(): Boolean + fun isBitcoinPizzaDayWallet(): Boolean + + fun isVeChainWallet(): Boolean + + fun isNewWorldEliteWallet(): Boolean + fun isWhiteWallet(): Boolean fun isWallet2(): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 077ffe5de1..8c3a6ec485 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -48,6 +48,12 @@ internal class TangemCardTypesResolver( override fun isSatoshiFriendsWallet(): Boolean = card.batchId == SATOSHI_WALLET_BATCH_ID + override fun isBitcoinPizzaDayWallet(): Boolean = card.batchId == BITCOIN_PIZZA_DAY_WALLET_BATCH_ID + + override fun isVeChainWallet(): Boolean = card.batchId == VECHAIN_WALLET_BATCH_ID + + override fun isNewWorldEliteWallet(): Boolean = card.batchId == NEW_WORLD_ELITE_WALLET_BATCH_ID + override fun isWhiteWallet(): Boolean { return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable } @@ -154,5 +160,8 @@ internal class TangemCardTypesResolver( const val WHITE_WALLET2_BATCH_ID = "AF15" const val TRILLIANT_WALLET_BATCH_ID = "AF16" const val AVRORA_WALLET_BATCH_ID = "AF18" + const val BITCOIN_PIZZA_DAY_WALLET_BATCH_ID = "AF33" + const val VECHAIN_WALLET_BATCH_ID = "AF29" + const val NEW_WORLD_ELITE_WALLET_BATCH_ID = "AF26" } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt index 966e1bcf83..8ede57bcf9 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt @@ -1,11 +1,11 @@ package com.tangem.domain.settings -import com.tangem.domain.settings.repositories.SwapPromoRepository +import com.tangem.domain.settings.repositories.PromoSettingsRepository import kotlinx.coroutines.flow.Flow -class ShouldShowSwapPromoTokenUseCase(private val swapPromoRepository: SwapPromoRepository) { +class ShouldShowSwapPromoTokenUseCase(private val promoSettingsRepository: PromoSettingsRepository) { - operator fun invoke(): Flow = swapPromoRepository.isReadyToShowTokenPromo() + operator fun invoke(): Flow = promoSettingsRepository.isReadyToShowTokenSwapPromo() - suspend fun neverToShow() = swapPromoRepository.setNeverToShowTokenPromo() + suspend fun neverToShow() = promoSettingsRepository.setNeverToShowTokenSwapPromo() } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt index e2b30e1ba5..233277f71f 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt @@ -1,11 +1,11 @@ package com.tangem.domain.settings -import com.tangem.domain.settings.repositories.SwapPromoRepository +import com.tangem.domain.settings.repositories.PromoSettingsRepository import kotlinx.coroutines.flow.Flow -class ShouldShowSwapPromoWalletUseCase(private val swapPromoRepository: SwapPromoRepository) { +class ShouldShowSwapPromoWalletUseCase(private val promoSettingsRepository: PromoSettingsRepository) { - operator fun invoke(): Flow = swapPromoRepository.isReadyToShowWalletPromo() + operator fun invoke(): Flow = promoSettingsRepository.isReadyToShowWalletSwapPromo() - suspend fun neverToShow() = swapPromoRepository.setNeverToShowWalletPromo() + suspend fun neverToShow() = promoSettingsRepository.setNeverToShowWalletSwapPromo() } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowTravalaPromoWalletUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowTravalaPromoWalletUseCase.kt new file mode 100644 index 0000000000..643e7387c8 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowTravalaPromoWalletUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.PromoSettingsRepository +import kotlinx.coroutines.flow.Flow + +class ShouldShowTravalaPromoWalletUseCase(private val promoSettingsRepository: PromoSettingsRepository) { + + operator fun invoke(): Flow = promoSettingsRepository.isReadyToShowWalletTravalaPromo() + + suspend fun neverToShow() = promoSettingsRepository.setNeverToShowWalletTravalaPromo() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt new file mode 100644 index 0000000000..0e3946bb89 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PromoSettingsRepository.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.settings.repositories + +import kotlinx.coroutines.flow.Flow + +interface PromoSettingsRepository { + fun isReadyToShowWalletSwapPromo(): Flow + + fun isReadyToShowTokenSwapPromo(): Flow + + suspend fun setNeverToShowWalletSwapPromo() + + suspend fun setNeverToShowTokenSwapPromo() + + fun isReadyToShowWalletTravalaPromo(): Flow + + suspend fun setNeverToShowWalletTravalaPromo() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt deleted file mode 100644 index ca97755e66..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.domain.settings.repositories - -import kotlinx.coroutines.flow.Flow - -interface SwapPromoRepository { - fun isReadyToShowWalletPromo(): Flow - - fun isReadyToShowTokenPromo(): Flow - - suspend fun setNeverToShowWalletPromo() - - suspend fun setNeverToShowTokenPromo() -} \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index c0a73ff9fd..501cf70dd8 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { /** Project - Other */ implementation(projects.core.utils) + implementation(projects.libs.crypto) /** Android - Other */ implementation(deps.androidx.paging.runtime) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/promo/PromoBanner.kt b/domain/tokens/models/src/main/java/com/tangem/domain/promo/PromoBanner.kt index 564a49e3d4..2608085143 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/promo/PromoBanner.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/promo/PromoBanner.kt @@ -12,6 +12,7 @@ data class PromoBanner( data class BannerState( val timeline: Timeline, val status: String, + val link: String?, ) data class Timeline( diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index 5c1cad5f44..86b0059faf 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -45,4 +45,6 @@ sealed class CryptoCurrencyWarning { val startDateTime: DateTime, val endDateTime: DateTime, ) : CryptoCurrencyWarning() + + data object BeaconChainShutdown : CryptoCurrencyWarning() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 43a7a2fda8..5036e8f341 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -12,6 +12,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching import com.tangem.utils.isNullOrZero @@ -74,6 +75,7 @@ class GetCurrencyWarningsUseCase( *coinRelatedWarnings.toTypedArray(), getNetworkUnavailableWarning(currencyStatus), getNetworkNoAccountWarning(currencyStatus), + getBeaconChainShutdownWarning(currency.network.id), ) }.flowOn(dispatchers.io) } @@ -261,6 +263,10 @@ class GetCurrencyWarningsUseCase( } } + private fun getBeaconChainShutdownWarning(networkId: Network.ID): CryptoCurrencyWarning.BeaconChainShutdown? { + return if (BlockchainUtils.isBeaconChain(networkId.value)) CryptoCurrencyWarning.BeaconChainShutdown else null + } + private fun BigDecimal?.isZero(): Boolean { return this?.signum() == 0 } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkAddressesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkAddressesUseCase.kt new file mode 100644 index 0000000000..0cc768a333 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkAddressesUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetNetworkAddressesUseCase( + internal val networksRepository: NetworksRepository, +) { + + operator fun invoke(userWalletId: UserWalletId, network: Network): Flow = + networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network)) + .map { networkStatuses -> + when (val networkStatus = networkStatuses.singleOrNull { it.network.id == network.id }?.value) { + is NetworkStatus.NoAccount -> networkStatus.address.defaultAddress.value + is NetworkStatus.Unreachable -> networkStatus.address?.defaultAddress?.value.orEmpty() + is NetworkStatus.Verified -> networkStatus.address.defaultAddress.value + else -> "" + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt index b15e05db54..ed42ab1cfc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt @@ -5,4 +5,6 @@ import com.tangem.domain.promo.PromoBanner interface PromoRepository { suspend fun getChangellyPromoBanner(): PromoBanner? + + suspend fun getTravalaPromoBanner(): PromoBanner? } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt index 76872f50ff..a1f077948d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt @@ -3,7 +3,7 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow - +import kotlinx.coroutines.flow.firstOrNull /** * Use case for getting list of user wallets * @@ -15,4 +15,7 @@ class GetWalletsUseCase(private val userWalletsListManager: UserWalletsListManag @Throws(IllegalArgumentException::class) operator fun invoke(): Flow> = userWalletsListManager.userWallets + + @Throws(IllegalArgumentException::class) + suspend fun invokeSync(): List? = userWalletsListManager.userWallets.firstOrNull() } \ No newline at end of file diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt index d80f08cf56..453a79b60c 100644 --- a/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt +++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/featuretoggles/SendFeatureToggles.kt @@ -7,4 +7,7 @@ interface SendFeatureToggles { /** Availability of redesigned send screen */ val isRedesignedSendEnabled: Boolean + + /** Updates remote toggle */ + suspend fun fetchNewSendEnabled() } \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 43a930c199..66e103efad 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.analytics) implementation(projects.core.analytics.models) + implementation(projects.core.datasource) /** Common */ implementation(projects.common) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt index 6b1465762c..60c8b2822c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendFeatureTogglesModule.kt @@ -1,8 +1,10 @@ package com.tangem.features.send.impl.di import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.features.send.impl.featuretoggles.DefaultSendFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,7 +20,15 @@ internal object SendFeatureTogglesModule { @Provides @Singleton - fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): SendFeatureToggles { - return DefaultSendFeatureToggles(featureTogglesManager = featureTogglesManager) + fun provideSendFeatureToggles( + featureTogglesManager: FeatureTogglesManager, + tangemTechApi: TangemTechApi, + dispatchers: CoroutineDispatcherProvider, + ): SendFeatureToggles { + return DefaultSendFeatureToggles( + featureTogglesManager = featureTogglesManager, + tangemTechApi = tangemTechApi, + dispatchers = dispatchers, + ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt index 71f11c480d..b36cba4cbd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/featuretoggles/DefaultSendFeatureToggles.kt @@ -1,16 +1,41 @@ package com.tangem.features.send.impl.featuretoggles import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.features.send.api.featuretoggles.SendFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber /** * Default implementation of Send feature toggles * * @property featureTogglesManager manager for getting information about the availability of feature toggles + * @property tangemTechApi api to get remote feature toggle for send + * @property dispatchers coroutine dispatchers */ internal class DefaultSendFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, + private val tangemTechApi: TangemTechApi, + private val dispatchers: CoroutineDispatcherProvider, ) : SendFeatureToggles { + + private val remoteSendEnabled: MutableStateFlow = MutableStateFlow(true) + override val isRedesignedSendEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_SEND_SCREEN_ENABLED") + get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_SEND_SCREEN_ENABLED") && + remoteSendEnabled.value + + override suspend fun fetchNewSendEnabled() { + runCatching(dispatchers.io) { + tangemTechApi.getFeatures().getOrThrow() + }.onSuccess { response -> + remoteSendEnabled.update { response.isNewSendEnabled } + }.onFailure { + Timber.e(it.localizedMessage, "Unable to fetch new send toggle") + } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt index feca8a0786..c4ce202599 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/SendAnalyticEvents.kt @@ -1,7 +1,9 @@ package com.tangem.features.send.impl.presentation.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE @@ -66,9 +68,9 @@ internal sealed class SendAnalyticEvents( data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened") /** Selected fee (send after next screen opened) */ - data class SelectedFee(val feeType: SelectedFeeType) : SendAnalyticEvents( + data class SelectedFee(val feeType: AnalyticsParam.FeeType) : SendAnalyticEvents( event = "Fee Selected", - params = mapOf("Fee Type" to feeType.name), + params = mapOf("Fee Type" to feeType.value), ) /** Custom fee selected */ @@ -97,7 +99,16 @@ internal sealed class SendAnalyticEvents( // region Transaction Result /** Transaction send screen opened */ - data object TransactionScreenOpened : SendAnalyticEvents(event = "Transaction Sent Screen Opened") + data class TransactionScreenOpened( + val token: String, + val feeType: AnalyticsParam.FeeType, + ) : SendAnalyticEvents( + event = "Transaction Sent Screen Opened", + params = mapOf( + TOKEN to token, + FEE_TYPE to feeType.value, + ), + ) /** Share button clicked */ data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share") @@ -145,12 +156,4 @@ internal enum class EnterAddressSource { internal enum class SelectedCurrencyType(val value: String) { Token("Token"), AppCurrency("App Currency"), -} - -internal enum class SelectedFeeType { - Min, - Max, - Fixed, - Normal, - Custom, } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt index 27c93b9743..ac6485fc8a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt @@ -2,8 +2,10 @@ package com.tangem.features.send.impl.presentation.analytics.utils import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.send.impl.presentation.analytics.SelectedCurrencyType -import com.tangem.features.send.impl.presentation.analytics.SelectedFeeType import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents import com.tangem.features.send.impl.presentation.analytics.SendScreenSource import com.tangem.features.send.impl.presentation.state.SendUiState @@ -11,11 +13,13 @@ import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType +import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.utils.Provider internal class SendScreenAnalyticSender( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, + private val cryptoCurrencyProvider: Provider, private val analyticsEventHandler: AnalyticsEventHandler, ) { fun send(prevScreen: SendUiStateType, state: SendUiState) { @@ -73,16 +77,57 @@ internal class SendScreenAnalyticSender( ) } + fun sendTransaction() { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val cryptoCurrency = cryptoCurrencyProvider() + val feeState = state.getFeeState(isEditState) ?: return + val recipientState = state.getRecipientState(isEditState) ?: return + + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return + val feeType = getSendTransactionFeeType(feeSelectorState) + analyticsEventHandler.send( + SendAnalyticEvents.TransactionScreenOpened( + token = cryptoCurrency.symbol, + feeType = feeType, + ), + ) + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = AnalyticsParam.TxSentFrom.Send( + blockchain = cryptoCurrency.network.name, + token = cryptoCurrency.symbol, + feeType = feeType, + ), + memoType = getSendTransactionMemoType(recipientState.memoTextField), + ), + ) + } + private fun sendSelectedFeeAnalytics(feeSelectorState: FeeSelectorState.Content) { - val type = when (feeSelectorState.fees) { - is TransactionFee.Single -> SelectedFeeType.Fixed - is TransactionFee.Choosable -> when (feeSelectorState.selectedFee) { - FeeType.Slow -> SelectedFeeType.Min - FeeType.Market -> SelectedFeeType.Normal - FeeType.Fast -> SelectedFeeType.Max - FeeType.Custom -> SelectedFeeType.Custom - } - } + val type = getSendTransactionFeeType(feeSelectorState) analyticsEventHandler.send(SendAnalyticEvents.SelectedFee(type)) } + + private fun getSendTransactionFeeType(feeSelectorState: FeeSelectorState.Content): AnalyticsParam.FeeType = + when (feeSelectorState.fees) { + is TransactionFee.Single -> AnalyticsParam.FeeType.Fixed + is TransactionFee.Choosable -> when (feeSelectorState.selectedFee) { + FeeType.Slow -> AnalyticsParam.FeeType.Min + FeeType.Market -> AnalyticsParam.FeeType.Normal + FeeType.Fast -> AnalyticsParam.FeeType.Max + FeeType.Custom -> AnalyticsParam.FeeType.Custom + } + } + + private fun getSendTransactionMemoType( + recipientMemo: SendTextField.RecipientMemo?, + ): Basic.TransactionSent.MemoType { + val memo = recipientMemo?.value + return when { + memo?.isBlank() == true -> Basic.TransactionSent.MemoType.Empty + memo?.isNotBlank() == true -> Basic.TransactionSent.MemoType.Full + else -> Basic.TransactionSent.MemoType.Null + } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt index 69e56bfc62..8a7d5e7f3a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/AvailableWallet.kt @@ -1,15 +1,18 @@ package com.tangem.features.send.impl.presentation.domain import androidx.compose.runtime.Immutable +import com.tangem.domain.wallets.models.UserWalletId /** * Available wallet to send * * @property name wallet name + * @property userWalletId wallet id * @property address blockchain address */ @Immutable data class AvailableWallet( val name: String, + val userWalletId: UserWalletId, val address: String, ) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index a111cac1e9..f2bf0e4d67 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -54,7 +54,7 @@ internal sealed class SendNotification(val config: NotificationConfig) { wrappedList(cryptoCurrency, utxoLimit, amountLimit), ), buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(R.string.send_notification_reduce_to, wrappedList(amountLimit)), + text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)), onClick = onConfirmClick, ), ) @@ -98,7 +98,7 @@ internal sealed class SendNotification(val config: NotificationConfig) { title = resourceReference(R.string.send_notification_existential_deposit_title), subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(R.string.send_notification_existential_deposit_button, wrappedList(deposit)), + text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)), onClick = onConfirmClick, ), ) @@ -119,12 +119,13 @@ internal sealed class SendNotification(val config: NotificationConfig) { ), ) { data class HighFeeError( + val currencyName: String, val amount: String, val onConfirmClick: () -> Unit, val onCloseClick: () -> Unit, ) : Warning( title = resourceReference(R.string.send_notification_high_fee_title), - subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)), + subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)), buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)), onClick = onConfirmClick, @@ -156,7 +157,7 @@ internal sealed class SendNotification(val config: NotificationConfig) { data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning( title = resourceReference(R.string.send_network_fee_warning_title), subtitle = resourceReference( - R.string.send_network_fee_warning_content, + R.string.common_network_fee_warning_content, wrappedList(cryptoAmount, fiatAmount), ), ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 57bb2ccffc..377143195d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -72,11 +72,12 @@ internal class SendNotificationFactory( amountValue = amountValue, feeValue = feeValue, ) - val sendingAmount = calculateSubtractedAmount( - isFeeCoverage = isFeeCoverage, + val sendingAmount = checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isFeeCoverage, cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), amountValue = amountValue, feeValue = feeValue, + reduceAmountBy = sendState.reduceAmountBy, ) buildList { // errors @@ -204,7 +205,7 @@ internal class SendNotificationFactory( val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) { feeAmount } else { - feeAmount + receivedAmount + receivedAmount } val currencyDeposit = currencyChecksRepository.getExistentialDeposit( userWalletId, @@ -270,6 +271,7 @@ internal class SendNotificationFactory( if (!ignoreAmountReduce && isTotalBalance && isTezos) { add( SendNotification.Warning.HighFeeError( + currencyName = cryptoCurrencyStatus.currency.name, amount = threshold.toPlainString(), onConfirmClick = { clickIntents.onAmountReduceClick( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt index b06d42a23d..a8a93bcd32 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeCalculation.kt @@ -54,7 +54,7 @@ internal fun checkFeeCoverage( /** * Calculates subtracted amount */ -internal fun calculateSubtractedAmount( +private fun calculateSubtractedAmount( isFeeCoverage: Boolean, cryptoCurrencyStatus: CryptoCurrencyStatus, amountValue: BigDecimal, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt index a2a13e987a..62bf1460ad 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt @@ -47,7 +47,7 @@ internal class BitcoinCustomFeeConverter( keyboardType = KeyboardType.Number, ), title = resourceReference(R.string.send_max_fee), - footer = resourceReference(R.string.send_max_fee_footer), + footer = resourceReference(R.string.send_bitcoin_custom_fee_footer), label = getFiatReference( rate = feeCurrency?.fiatRate, value = feeValue, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index 6bed843250..d64f1c3d47 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -43,7 +43,7 @@ internal class EthereumCustomFeeConverter( keyboardType = KeyboardType.Number, ), title = resourceReference(R.string.send_max_fee), - footer = resourceReference(R.string.send_max_fee_footer), + footer = resourceReference(R.string.send_evm_custom_fee_footer), label = getFiatReference( rate = feeCurrency?.fiatRate, value = feeValue, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt index 7d4dfe3b45..8f3d03dbbc 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt @@ -10,6 +10,7 @@ import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero +import java.math.RoundingMode internal class SendAmountFieldMaxAmountConverter( private val stateRouterProvider: Provider, @@ -33,7 +34,7 @@ internal class SendAmountFieldMaxAmountConverter( val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero() val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty() - val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals).orEmpty() + val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() return state.copyWrapped( isEditState = isEditState, amountState = amountState.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index e858d9b150..c3167929ad 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -4,7 +4,10 @@ import androidx.compose.animation.* import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +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.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -26,12 +29,15 @@ import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.shareText +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType +import com.tangem.features.send.impl.presentation.utils.getFiatFormatted @Composable internal fun SendNavigationButtons( @@ -172,21 +178,27 @@ private fun SendingText( } if (feeFiat != null && sendingFiat != null) { - val sendingValue = BigDecimalFormatter.formatFiatAmount( - fiatAmount = sendingFiat, - fiatCurrencyCode = feeState.appCurrency.code, - fiatCurrencySymbol = feeState.appCurrency.symbol, + val sendingValue = getFiatFormatted( + value = sendingFiat, + currencySymbol = feeState.appCurrency.symbol, + currencyCode = feeState.appCurrency.code, ) - val feeValue = BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeFiat, - fiatCurrencyCode = feeState.appCurrency.code, - fiatCurrencySymbol = feeState.appCurrency.symbol, + val feeValue = getFiatFormatted( + value = feeState.fee?.amount?.value, + currencySymbol = feeState.appCurrency.symbol, + currencyCode = feeState.appCurrency.code, ) + val textResource = remember(sendingValue, feeValue) { + resourceReference( + id = R.string.send_summary_transaction_description, + formatArgs = wrappedList(sendingValue, feeValue), + ) + } Text( - text = stringResource(id = R.string.send_summary_transaction_description, sendingValue, feeValue), + text = textResource.resolveAnnotatedReference(), textAlign = TextAlign.Center, - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, modifier = Modifier .fillMaxWidth() .padding(TangemTheme.dimens.spacing12), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index d6529709b7..819655152a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -33,7 +33,10 @@ import kotlinx.coroutines.flow.withIndex @Composable internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) { val snackbarHostState = remember { SnackbarHostState() } - BackHandler(onBack = uiState.clickIntents::onBackClick) + val onBackClick = uiState.clickIntents::onBackClick.takeIf { + uiState.sendState?.isSending != true + } ?: {} + BackHandler(onBack = onBackClick) Column( modifier = Modifier .fillMaxSize() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt index fcff795b70..929cfdf3b2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/Notifications.kt @@ -15,6 +15,7 @@ internal fun LazyListScope.notifications( notifications: ImmutableList, modifier: Modifier = Modifier, hasPaddingAbove: Boolean = false, + isClickDisabled: Boolean = false, ) { itemsIndexed( items = notifications, @@ -44,6 +45,7 @@ internal fun LazyListScope.notifications( -> null is SendNotification.Error -> TangemTheme.colors.icon.warning }, + isEnabled = !isClickDisabled, ) }, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index d9c609a5ca..d29fea2394 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -16,6 +16,7 @@ import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType @@ -114,11 +115,14 @@ private fun FeeError(feeSelectorState: FeeSelectorState) { private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? { val choosableFees = fees as? TransactionFee.Choosable + val decimals = fees.normal.amount.decimals + val customValue = this.customValues.firstOrNull()?.value?.parseToBigDecimal(decimals) + val customAmount = fees.normal.amount.copy(value = customValue) return when (feeType) { FeeType.Slow -> choosableFees?.minimum?.amount FeeType.Market -> fees.normal.amount FeeType.Fast -> choosableFees?.priority?.amount - FeeType.Custom -> null + FeeType.Custom -> customAmount } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index 5c0ca33a84..c59617aac0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -33,9 +33,10 @@ internal fun TextFieldWithPaste( ) { val (title, color) = when { isError && error != null -> error to TangemTheme.colors.text.warning - isReadOnly -> label to TangemTheme.colors.text.disabled + isReadOnly -> label to TangemTheme.colors.text.tertiary else -> label to TangemTheme.colors.text.secondary } + val placeholderColor = if (isReadOnly) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.disabled FooterContainer(modifier, footer) { Box( modifier = Modifier @@ -59,6 +60,7 @@ internal fun TextFieldWithPaste( SimpleTextField( value = value, placeholder = placeholder, + placeholderColor = placeholderColor, onValueChange = onValueChange, readOnly = isReadOnly, modifier = Modifier diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt index 3533b2a175..e994f44e6f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt @@ -26,7 +26,7 @@ import com.tangem.features.send.impl.presentation.state.previewdata.AmountStateP @Composable internal fun AmountBlock( amountState: SendStates.AmountState, - isSuccess: Boolean, + isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit, ) { @@ -55,7 +55,7 @@ internal fun AmountBlock( modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(backgroundColor) - .clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick) + .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding( vertical = TangemTheme.dimens.spacing14, horizontal = TangemTheme.dimens.spacing16, @@ -92,7 +92,7 @@ private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::cla TangemThemePreview { AmountBlock( amountState = value, - isSuccess = false, + isClickDisabled = false, isEditingDisabled = false, onClick = {}, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index 9e762ae382..68dc4c0c8b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -28,13 +28,13 @@ import com.tangem.features.send.impl.presentation.utils.getCryptoReference import com.tangem.features.send.impl.presentation.utils.getFiatReference @Composable -internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { +internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, onClick: () -> Unit) { Column( modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) - .clickable(enabled = !isSuccess, onClick = onClick) + .clickable(enabled = !isClickDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing12), ) { Text( @@ -118,7 +118,7 @@ private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) va TangemThemePreview { FeeBlock( feeState = value, - isSuccess = true, + isClickDisabled = true, onClick = {}, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt index 0b4752d640..4839b83690 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt @@ -25,7 +25,7 @@ import com.tangem.features.send.impl.presentation.state.previewdata.RecipientSta @Composable internal fun RecipientBlock( recipientState: SendStates.RecipientState, - isSuccess: Boolean, + isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit, ) { @@ -40,7 +40,7 @@ internal fun RecipientBlock( .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(backgroundColor) - .clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick) + .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing12), ) { AddressBlock(recipientState.addressTextField) @@ -107,7 +107,7 @@ private fun RecipientBlockPreview( TangemThemePreview { RecipientBlock( recipientState = value, - isSuccess = true, + isClickDisabled = true, isEditingDisabled = false, onClick = {}, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 6a6cc0550d..6c6de6e979 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -35,12 +35,13 @@ private const val TAP_HELP_ANIMATION_DELAY = 500L @Composable internal fun SendContent(uiState: SendUiState) { val sendState = uiState.sendState ?: return + val isClickDisabled = sendState.isSending || sendState.isSuccess LazyColumn( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { blocks(uiState) tapHelp(isDisplay = sendState.showTapHelp) - notifications(sendState.notifications) + notifications(notifications = sendState.notifications, isClickDisabled = isClickDisabled) } } @@ -50,6 +51,7 @@ private fun LazyListScope.blocks(uiState: SendUiState) { val feeState = uiState.feeState ?: return val sendState = uiState.sendState ?: return val isSuccess = sendState.isSuccess + val isClickDisabled = sendState.isSending || isSuccess val timestamp = sendState.transactionDate item(key = BLOCKS_KEY) { @@ -65,19 +67,19 @@ private fun LazyListScope.blocks(uiState: SendUiState) { } RecipientBlock( recipientState = recipientState, - isSuccess = isSuccess, + isClickDisabled = isClickDisabled, isEditingDisabled = uiState.isEditingDisabled, onClick = uiState.clickIntents::showRecipient, ) AmountBlock( amountState = amountState, - isSuccess = isSuccess, + isClickDisabled = isClickDisabled, isEditingDisabled = uiState.isEditingDisabled, onClick = uiState.clickIntents::showAmount, ) FeeBlock( feeState = feeState, - isSuccess = isSuccess, + isClickDisabled = isClickDisabled, onClick = uiState.clickIntents::showFee, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt index 73ef044814..f2be7e6ed2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt @@ -11,6 +11,7 @@ import java.math.BigDecimal import java.math.RoundingMode private const val FIAT_DECIMALS = 2 +private const val CRYPTO_FEE_DECIMALS = 6 private const val FEE_MINIMUM_VALUE = 0.01 internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { @@ -21,7 +22,7 @@ internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): Tex BigDecimalFormatter.formatCryptoAmount( cryptoAmount = amount.value, cryptoCurrency = amount.currencySymbol, - decimals = amount.decimals, + decimals = amount.decimals.coerceAtMost(CRYPTO_FEE_DECIMALS), ), ), ) @@ -36,24 +37,27 @@ internal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { if (value == null || rate == null) return EMPTY_BALANCE_SIGN val feeValue = value.multiply(rate) - val scaled = feeValue.setScale(FIAT_DECIMALS, RoundingMode.UP) ?: BigDecimal.ZERO - val formattedValue = if (scaled < BigDecimal(FEE_MINIMUM_VALUE)) { + return getFiatFormatted(feeValue, appCurrency.code, appCurrency.symbol) +} + +internal fun getFiatFormatted(value: BigDecimal?, currencyCode: String, currencySymbol: String): String { + val scaled = value?.setScale(FIAT_DECIMALS, RoundingMode.UP) ?: BigDecimal.ZERO + return if (scaled < BigDecimal(FEE_MINIMUM_VALUE)) { buildString { append(BigDecimalFormatter.CAN_BE_LOWER_SIGN) append( BigDecimalFormatter.formatFiatAmount( fiatAmount = BigDecimal(FEE_MINIMUM_VALUE), - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, + fiatCurrencyCode = currencyCode, + fiatCurrencySymbol = currencySymbol, ), ) } } else { BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, + fiatAmount = value, + fiatCurrencyCode = currencyCode, + fiatCurrencySymbol = currencySymbol, ) } - return formattedValue } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index c90a473a60..98d771ee76 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -26,7 +26,6 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.GetFeeError @@ -56,8 +55,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.* +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import java.util.Locale @@ -76,8 +76,8 @@ internal class SendViewModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, - private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, - private val getCryptoCurrencyStatusesSyncUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, + private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, @@ -92,6 +92,7 @@ internal class SendViewModel @Inject constructor( private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, validateTransactionUseCase: ValidateTransactionUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, @@ -179,6 +180,7 @@ internal class SendViewModel @Inject constructor( stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState }, analyticsEventHandler = analyticsEventHandler, + cryptoCurrencyProvider = Provider { cryptoCurrency }, ) } @@ -187,6 +189,7 @@ internal class SendViewModel @Inject constructor( private set private var userWallet: UserWallet by Delegates.notNull() + private var userWallets: List = emptyList() private var isAmountSubtractAvailable: Boolean = false private var isTapHelpPreviewEnabled: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() @@ -399,57 +402,48 @@ internal class SendViewModel @Inject constructor( } private fun getUserWallets() { - getWalletsUseCase() - .conflate() - .distinctUntilChanged() - .onEach { userWallets -> - coroutineScope { - runCatching { - userWallets - .filterNot { it.walletId == userWalletId || it.isLocked } - .map { wallet -> - async(dispatchers.io) { wallet.toAvailableWallet() } - }.awaitAll() - }.onSuccess { result -> - uiState = stateFactory.onLoadedWalletsList(wallets = result) - }.onFailure { - uiState = stateFactory.onLoadedWalletsList(wallets = emptyList()) + viewModelScope.launch(dispatchers.main) { + runCatching { + getWalletsUseCase.invokeSync() + ?.toAvailableWallets() + .orEmpty() + }.onSuccess { result -> + combine(*result.toTypedArray()) { it } + .onEach { + userWallets = it.filterNotNull().toList() + uiState = stateFactory.onLoadedWalletsList(wallets = userWallets) } - } - } - .flowOn(dispatchers.main) - .launchIn(viewModelScope) - } - - private suspend fun UserWallet.toAvailableWallet(): AvailableWallet? { - return if (!isMultiCurrency) { - val status = getCryptoCurrencyStatusSyncUseCase(walletId).getOrNull() - val address = status?.value?.networkAddress.takeIf { - status?.currency?.network?.id == cryptoCurrency.network.id && - status.currency.network.derivationPath !is Network.DerivationPath.Custom - } - address?.let { - AvailableWallet( - name = name, - address = it.defaultAddress.value, - ) - } - } else { - val statuses = getCryptoCurrencyStatusesSyncUseCase(walletId).getOrNull() - val walletCurrency = statuses?.firstOrNull { - it.currency.network.id == cryptoCurrency.network.id && - it.currency.network.derivationPath !is Network.DerivationPath.Custom - } - val address = walletCurrency?.value?.networkAddress - address?.let { - AvailableWallet( - name = name, - address = it.defaultAddress.value, - ) + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + }.onFailure { + uiState = stateFactory.onLoadedWalletsList(wallets = emptyList()) } } } + private suspend fun List.toAvailableWallets(): List> = + filterNot { it.walletId == userWalletId || it.isLocked } + .mapNotNull { wallet -> + val status = if (!wallet.isMultiCurrency) { + getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { + if (it.network.id == cryptoCurrency.network.id) { + getNetworkAddressesUseCase(wallet.walletId, it.network) + } else { + null + } + } + } else { + getNetworkAddressesUseCase(wallet.walletId, cryptoCurrency.network) + } + status?.map { address -> + AvailableWallet( + name = wallet.name, + address = address, + userWalletId = wallet.walletId, + ) + } + } + private suspend fun getTxHistory() { val txHistoryList = getFixedTxHistoryItemsUseCase.getSync( userWalletId = userWalletId, @@ -874,11 +868,25 @@ internal class SendViewModel @Inject constructor( uiState = stateFactory.getSendingStateUpdate(isSending = false) updateTransactionStatus(txData) scheduleBalanceUpdate() - analyticsEventHandler.send(SendAnalyticEvents.TransactionScreenOpened) + addTokenToWalletIfNeeded() + sendScreenAnalyticSender.sendTransaction() }, ) } + private fun addTokenToWalletIfNeeded() { + if (cryptoCurrency !is CryptoCurrency.Token) return + + val recipientState = uiState.getRecipientState(stateRouter.isEditState) ?: return + val destinationAddress = recipientState.addressTextField.value + + val maybeUserWallet = userWallets.firstOrNull { it.address == destinationAddress } ?: return + + viewModelScope.launch(dispatchers.io) { + addCryptoCurrenciesUseCase(userWalletId = maybeUserWallet.userWalletId, currency = cryptoCurrency) + } + } + private suspend fun updateTransactionStatus(txData: TransactionData) { val txUrl = getExplorerTransactionUrlUseCase( userWalletId = userWalletId, 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 4e12e4c227..26e4ec6069 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 @@ -216,6 +216,7 @@ internal class StateBuilder( val warnings = getWarningsForSuccessState( quoteModel = quoteModel, fromToken = fromToken, + selectedFeeType = selectedFeeType, ) val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus @@ -314,12 +315,14 @@ internal class StateBuilder( private fun getWarningsForSuccessState( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, + ignoreAmountReduce: Boolean, + selectedFeeType: FeeType, ): List { val warnings = mutableListOf() maybeAddDomainWarnings(quoteModel, warnings) maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken) - maybeAddNetworkFeeCoverageWarning(quoteModel, warnings) + maybeAddNetworkFeeCoverageWarning(quoteModel, warnings, selectedFeeType) maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings) maybeAddInsufficientFundsWarning(quoteModel, warnings) maybeAddTransactionInProgressWarning(quoteModel, warnings) @@ -460,18 +463,37 @@ internal class StateBuilder( private fun maybeAddNetworkFeeCoverageWarning( quoteModel: SwapState.QuotesLoadedState, warnings: MutableList, + selectedFeeType: FeeType, ) { when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { - is IncludeFeeInAmount.Included -> + is IncludeFeeInAmount.Included -> { + val fee = selectFeeByType(selectedFeeType, quoteModel.txFee) ?: return warnings.add( SwapWarning.GeneralWarning( - createNetworkFeeCoverageNotificationConfig(), + createNetworkFeeCoverageNotificationConfig( + quoteModel.fromTokenInfo.tokenAmount.getFormattedCryptoAmount( + quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency, + ), + fee.feeFiatFormatted, + ), ), ) + } else -> Unit } } + private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): TxFee? { + return when (txFeeState) { + TxFeeState.Empty -> null + is TxFeeState.SingleFeeState -> txFeeState.fee + is TxFeeState.MultipleFeeState -> when (feeType) { + FeeType.NORMAL -> txFeeState.normalFee + FeeType.PRIORITY -> txFeeState.priorityFee + } + } + } + private fun maybeAddUnableCoverFeeWarning( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, @@ -547,12 +569,12 @@ internal class StateBuilder( if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder val warnings = mutableListOf() warnings.add(getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency)) - if (includeFeeInAmount is IncludeFeeInAmount.Included) { - warnings.add( - SwapWarning.GeneralWarning( - createNetworkFeeCoverageNotificationConfig(), - ), + if (includeFeeInAmount is IncludeFeeInAmount.Included && uiStateHolder.fee is FeeItemState.Content) { + val feeCoverageNotification = createNetworkFeeCoverageNotificationConfig( + fromToken.tokenAmount.getFormattedCryptoAmount(fromToken.cryptoCurrencyStatus.currency), + uiStateHolder.fee.amountFiatFormatted, ) + warnings.add(SwapWarning.GeneralWarning(feeCoverageNotification)) } val providerState = getProviderStateForError( swapProvider = swapProvider, @@ -1402,10 +1424,16 @@ internal class StateBuilder( ) } - private fun createNetworkFeeCoverageNotificationConfig(): NotificationConfig { + private fun createNetworkFeeCoverageNotificationConfig( + cryptoAmount: String, + fiatAmount: String, + ): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.send_network_fee_warning_title), - subtitle = resourceReference(R.string.swapping_network_fee_warning_content), + subtitle = resourceReference( + R.string.common_network_fee_warning_content, + wrappedList(cryptoAmount, fiatAmount), + ), iconResId = R.drawable.img_attention_20, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index 9236e01d5e..b458c3f622 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -41,6 +41,7 @@ internal class TokenDetailsNotificationsAnalyticsSender( is TokenDetailsNotification.TopUpWithoutReserve, is TokenDetailsNotification.RentInfo, is TokenDetailsNotification.SwapPromo, + is TokenDetailsNotification.NetworkShutdown, -> null } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index bed8a2a0e2..1c192640f7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -155,7 +155,11 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ) - class NetworksNoAccount(val network: String, val symbol: String, val amount: String) : Informational( + data class NetworksNoAccount( + private val network: String, + private val symbol: String, + private val amount: String, + ) : Informational( title = resourceReference(R.string.warning_no_account_title), subtitle = resourceReference( id = R.string.no_account_generic, @@ -167,4 +171,17 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { title = resourceReference(id = R.string.warning_no_account_title), subtitle = resourceReference(id = R.string.no_account_send_to_create), ) + + class HasPendingTransactions(val coinSymbol: String) : Informational( + title = resourceReference(R.string.warning_send_blocked_pending_transactions_title), + subtitle = resourceReference( + id = R.string.warning_send_blocked_pending_transactions_message, + formatArgs = wrappedList(coinSymbol), + ), + ) + + data class NetworkShutdown(private val title: TextReference, private val subtitle: TextReference) : Warning( + title = title, + subtitle = subtitle, + ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 01535ea1b4..bb8d8c2c6b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -3,12 +3,15 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification.* import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.removeBy import kotlinx.collections.immutable.ImmutableList @@ -80,6 +83,10 @@ internal class TokenDetailsNotificationConverter( onSwapClick = clickIntents::onSwapPromoClick, onCloseClick = clickIntents::onSwapPromoDismiss, ) + is CryptoCurrencyWarning.BeaconChainShutdown -> NetworkShutdown( + title = resourceReference(R.string.warning_beacon_chain_retirement_title), + subtitle = resourceReference(R.string.warning_beacon_chain_retirement_content), + ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index de6ddf8b1b..f39a38511b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -121,4 +121,38 @@ sealed class WalletScreenAnalyticsEvent { data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") } + + sealed class Promotion( + event: String, + params: Map = mapOf(), + ) : AnalyticsEvent(category = "Promotion", event = event, params = params) { + class NoticePromotionBanner( + source: AnalyticsParam.ScreensSources, + programName: String, + ) : Promotion( + event = "Notice - Promotion Banner", + params = mapOf( + AnalyticsParam.SOURCE to source.value, + "Program Name" to programName, + ), + ) + + class PromotionBannerClicked( + source: AnalyticsParam.ScreensSources, + programName: String, + action: BannerAction, + ) : Promotion( + event = "Promo Banner Clicked", + params = mapOf( + AnalyticsParam.SOURCE to source.value, + "Program Name" to programName, + "Action" to action.action, + ), + ) { + sealed class BannerAction(val action: String) { + data object Clicked : BannerAction(action = "Clicked") + data object Closed : BannerAction(action = "Closed") + } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index ae05161912..59d4331387 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState @@ -44,6 +46,10 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem is WalletNotification.Critical.BackupError -> MainScreen.BackupError + is WalletNotification.TravalaPromo -> WalletScreenAnalyticsEvent.Promotion.NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + programName = "Travala", + ) is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] is WalletNotification.Informational.NoAccount, is WalletNotification.Warning.LowSignatures, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 51fbfe6742..a2adf6b292 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -6,7 +6,7 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.promo.PromoBanner import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase +import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -34,7 +34,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val getTokenListUseCase: GetTokenListUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, - private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, + private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase, private val promoRepository: PromoRepository, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, private val backupValidator: BackupValidator, @@ -45,18 +45,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver - val promoFlow = flow { emit(promoRepository.getChangellyPromoBanner()) } + val travalaPromoFlow = flow { emit(promoRepository.getTravalaPromoBanner()) } return combine( flow = getTokenListUseCase.launch(userWallet.walletId).conflate(), flow2 = isReadyToShowRateAppUseCase().conflate(), flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), - flow4 = shouldShowSwapPromoWalletUseCase().conflate(), - flow5 = promoFlow, - ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner -> + flow4 = shouldShowTravalaPromoWalletUseCase().conflate(), + flow5 = travalaPromoFlow.conflate(), + ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowTravalaPromo, promoBanner -> readyForRateAppNotification = true buildList { - addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents) + addTravalaPromoNotification(shouldShowTravalaPromo, promoBanner, clickIntents) addCriticalNotifications(userWallet) @@ -69,16 +69,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( } } - private fun MutableList.addSwapPromoNotification( + private fun MutableList.addTravalaPromoNotification( shouldShowPromo: Boolean, promoBanner: PromoBanner?, clickIntents: WalletClickIntents, ) { promoBanner ?: return - val promoNotification = WalletNotification.SwapPromo( + val promoNotification = WalletNotification.TravalaPromo( startDateTime = promoBanner.bannerState.timeline.start, endDateTime = promoBanner.bannerState.timeline.end, - onCloseClick = clickIntents::onCloseSwapPromoClick, + bannerLink = promoBanner.bannerState.link, + onBookNowButtonClick = clickIntents::onTravalaPromoClick, + onCloseClick = clickIntents::onCloseTravalaPromoClick, ) addIf( element = promoNotification, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index 7c7adbde20..3613a6439a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -34,6 +34,9 @@ internal object WalletImageResolver { cardTypesResolver.isJrWallet() -> userWallet.resolveJrWallet() cardTypesResolver.isGrimWallet() -> userWallet.resolveGrimWallet() cardTypesResolver.isSatoshiFriendsWallet() -> userWallet.resolveSatoshiWallet() + cardTypesResolver.isBitcoinPizzaDayWallet() -> userWallet.resolveBitcoinPizzaDayWallet() + cardTypesResolver.isVeChainWallet() -> userWallet.resolveVeChainWallet() + cardTypesResolver.isNewWorldEliteWallet() -> userWallet.resolveNewWorldEliteWallet() cardTypesResolver.isWallet2() -> userWallet.resolveWallet2() cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet() cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1() @@ -130,6 +133,27 @@ internal object WalletImageResolver { ) } + private fun UserWallet.resolveBitcoinPizzaDayWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_pizza_day_card2_120_106, + twoBackupResId = R.drawable.ill_pizza_day_card3_120_106, + ) + } + + private fun UserWallet.resolveVeChainWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_vechain_card2_120_106, + twoBackupResId = R.drawable.ill_vechain_card3_120_106, + ) + } + + private fun UserWallet.resolveNewWorldEliteWallet(): Int? { + return resolveWallet2( + oneBackupResId = R.drawable.ill_nwe_card2_120_106, + twoBackupResId = R.drawable.ill_nwe_card3_120_106, + ) + } + private fun UserWallet.resolveWallet1(): Int? { return resolveWalletWithBackups { count -> when (count) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 9f7f5ea139..c34e986c29 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -2,10 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.feature.wallet.impl.R import org.joda.time.DateTime @@ -173,6 +171,34 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ) + data class TravalaPromo( + val startDateTime: DateTime, + val endDateTime: DateTime, + val bannerLink: String?, + val onBookNowButtonClick: (String?) -> Unit, + val onCloseClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(id = R.string.main_travala_promotion_title), + subtitle = resourceReference( + id = R.string.main_travala_promotion_description, + wrappedList( + DateTimeFormatters.formatDate(startDateTime, DateTimeFormatters.dateMMMMd), + DateTimeFormatters.formatDate(endDateTime, DateTimeFormatters.dateMMMMd), + ), + ), + // Stub. Travala has its own Composable implementation with correct img + iconResId = R.drawable.ic_star_24, + // Stub. Travala has its own Composable implementation with correct img + backgroundResId = R.drawable.ic_star_24, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + onClick = { onBookNowButtonClick(bannerLink) }, + text = resourceReference(R.string.main_travala_promotion_button), + ), + onCloseClick = onCloseClick, + ), + ) + data class SwapPromo( val startDateTime: DateTime, val endDateTime: DateTime, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 476f3701e4..6b783e0eb0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationWithBackground +import com.tangem.core.ui.components.notifications.TravalaNotificationWithBackground import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -25,23 +26,33 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.icon.warning - is WalletNotification.Informational -> TangemTheme.colors.icon.accent - is WalletNotification.RateApp -> TangemTheme.colors.icon.attention - is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 - else -> null - }, - ) + // TODO develop promo banner general component + when (it) { + is WalletNotification.SwapPromo -> { + NotificationWithBackground( + config = it.config, + modifier = modifier.animateItemPlacement(), + ) + } + is WalletNotification.TravalaPromo -> { + TravalaNotificationWithBackground( + config = it.config, + modifier = modifier.animateItemPlacement(), + ) + } + else -> { + Notification( + config = it.config, + modifier = modifier.animateItemPlacement(), + iconTint = when (it) { + is WalletNotification.Critical -> TangemTheme.colors.icon.warning + is WalletNotification.Informational -> TangemTheme.colors.icon.accent + is WalletNotification.RateApp -> TangemTheme.colors.icon.attention + is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 + else -> null + }, + ) + } } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 7e47a2a490..10d52a0047 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -1,8 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.card.DeleteSavedAccessCodesUseCase +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.UpdateWalletUseCase import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader @@ -32,9 +36,13 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, private val walletEventSender: WalletEventSender, private val walletScreenContentLoader: WalletScreenContentLoader, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val updateWalletUseCase: UpdateWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, + private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val reduxStateHolder: ReduxStateHolder, private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), WalletCardClickIntents { @@ -72,7 +80,18 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { viewModelScope.launch(dispatchers.main) { walletScreenContentLoader.cancel(userWalletId) + + val deletedUserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch + + deleteSavedAccessCodesUseCase(cardId = deletedUserWallet.cardId) + .onLeft { Timber.e(it.toString()) } + deleteWalletUseCase(userWalletId) + .onRight { + getSelectedWalletSyncUseCase().getOrNull()?.let { + reduxStateHolder.onUserWalletSelected(it) + } + } .onLeft { Timber.e(it.toString()) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 553dafc78b..418a4d8afd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -11,6 +11,7 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.NeverToSuggestRateAppUseCase import com.tangem.domain.settings.RemindToRateAppLaterUseCase import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase +import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType @@ -19,6 +20,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler @@ -56,6 +58,10 @@ internal interface WalletWarningsClickIntents { fun onCloseRateAppWarningClick() fun onCloseSwapPromoClick() + + fun onTravalaPromoClick(link: String?) + + fun onCloseTravalaPromoClick() } @Suppress("LongParameterList") @@ -75,6 +81,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val reduxStateHolder: ReduxStateHolder, private val dispatchers: CoroutineDispatcherProvider, private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, + private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -212,6 +219,34 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } + override fun onTravalaPromoClick(link: String?) { + analyticsEventHandler.send( + WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked( + source = AnalyticsParam.ScreensSources.Main, + programName = "Travala", + action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Clicked, + ), + ) + link?.let { + viewModelScope.launch(dispatchers.main) { + router.openUrl(link) + } + } + } + + override fun onCloseTravalaPromoClick() { + analyticsEventHandler.send( + WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked( + source = AnalyticsParam.ScreensSources.Main, + programName = "Travala", + action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Closed, + ), + ) + viewModelScope.launch(dispatchers.main) { + shouldShowTravalaPromoWalletUseCase.neverToShow() + } + } + private suspend fun getSelectedUserWallet(): UserWallet? { val userWalletId = stateHolder.getSelectedWalletId() return getUserWalletUseCase(userWalletId).getOrElse { diff --git a/features/wallet/impl/src/main/res/drawable/ill_nwe_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_nwe_card2_120_106.webp new file mode 100644 index 0000000000..b54656293e Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_nwe_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_nwe_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_nwe_card3_120_106.webp new file mode 100644 index 0000000000..feecda86b3 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_nwe_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card2_120_106.webp new file mode 100644 index 0000000000..2608db4c6e Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card3_120_106.webp new file mode 100644 index 0000000000..d47e557121 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_pizza_day_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_vechain_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_vechain_card2_120_106.webp new file mode 100644 index 0000000000..bb5f23d301 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_vechain_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_vechain_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_vechain_card3_120_106.webp new file mode 100644 index 0000000000..249477b20f Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_vechain_card3_120_106.webp differ diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 67d4201243..1da8d9fff1 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -83,6 +83,7 @@ web3j = "4.10.1" leakcanary = "2.13" decompose = "2.2.2" room = "2.6.1" +markdown = "0.7.2" # endregion Other libraries # region Tangem @@ -258,4 +259,5 @@ decompose-ext-compose = { module = "com.arkivanov.decompose:extensions-compose-j room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" } # endregion Other diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 4571c8a31e..35b8386cd9 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -49,4 +49,10 @@ object BlockchainUtils { val blockchain = Blockchain.fromId(networkId) return blockchain == Blockchain.Cardano } + + /** If current [networkId] is BeaconChain */ + fun isBeaconChain(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Binance || blockchain == Blockchain.BinanceTestnet + } } \ No newline at end of file