Updated on 2026-08-14
|
|
@ -8,6 +8,8 @@ import com.tangem.domain.demo.DemoConfig
|
||||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
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.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -69,4 +71,10 @@ internal object CardDomainModule {
|
||||||
): GetExtendedPublicKeyForCurrencyUseCase {
|
): GetExtendedPublicKeyForCurrencyUseCase {
|
||||||
return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository)
|
return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ViewModelScoped
|
||||||
|
fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase {
|
||||||
|
return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,7 +7,9 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
|
||||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||||
import com.tangem.domain.settings.*
|
import com.tangem.domain.settings.*
|
||||||
import com.tangem.domain.settings.repositories.AppRatingRepository
|
import com.tangem.domain.settings.repositories.AppRatingRepository
|
||||||
|
import com.tangem.domain.settings.repositories.PromoSettingsRepository
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
|
import com.tangem.tap.domain.TangemSdkManager
|
||||||
import com.tangem.domain.settings.repositories.SwapPromoRepository
|
import com.tangem.domain.settings.repositories.SwapPromoRepository
|
||||||
import com.tangem.tap.domain.sdk.TangemSdkManager
|
import com.tangem.tap.domain.sdk.TangemSdkManager
|
||||||
import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository
|
import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository
|
||||||
|
|
@ -108,17 +110,25 @@ internal object SettingsDomainModule {
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@ViewModelScoped
|
||||||
fun provideShouldShowSwapPromoWalletUseCase(
|
fun provideShouldShowSwapPromoWalletUseCase(
|
||||||
swapPromoRepository: SwapPromoRepository,
|
promoSettingsRepository: PromoSettingsRepository,
|
||||||
): ShouldShowSwapPromoWalletUseCase {
|
): ShouldShowSwapPromoWalletUseCase {
|
||||||
return ShouldShowSwapPromoWalletUseCase(swapPromoRepository)
|
return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ViewModelScoped
|
||||||
|
fun provideShouldShowTravalaPromoWalletUseCase(
|
||||||
|
promoSettingsRepository: PromoSettingsRepository,
|
||||||
|
): ShouldShowTravalaPromoWalletUseCase {
|
||||||
|
return ShouldShowTravalaPromoWalletUseCase(promoSettingsRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@ViewModelScoped
|
||||||
fun provideShouldShowSwapPromoTokenUseCase(
|
fun provideShouldShowSwapPromoTokenUseCase(
|
||||||
swapPromoRepository: SwapPromoRepository,
|
promoSettingsRepository: PromoSettingsRepository,
|
||||||
): ShouldShowSwapPromoTokenUseCase {
|
): ShouldShowSwapPromoTokenUseCase {
|
||||||
return ShouldShowSwapPromoTokenUseCase(swapPromoRepository)
|
return ShouldShowSwapPromoTokenUseCase(promoSettingsRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -346,4 +346,12 @@ internal object TokensDomainModule {
|
||||||
): RunPolkadotAccountHealthCheckUseCase {
|
): RunPolkadotAccountHealthCheckUseCase {
|
||||||
return RunPolkadotAccountHealthCheckUseCase(repository)
|
return RunPolkadotAccountHealthCheckUseCase(repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ViewModelScoped
|
||||||
|
fun provideGetNetworkStatusesUseCase(networksRepository: NetworksRepository): GetNetworkAddressesUseCase {
|
||||||
|
return GetNetworkAddressesUseCase(
|
||||||
|
networksRepository = networksRepository,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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<Throwable, Unit> {
|
||||||
|
tangemSdkManager.deleteSavedUserCodes(setOf(cardId))
|
||||||
|
.doOnFailure { return it.left() }
|
||||||
|
.doOnSuccess { return Unit.right() }
|
||||||
|
|
||||||
|
return Unit.right()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,15 +8,20 @@ internal class DelegatedKeystoreManager(
|
||||||
private val keystoreManagerProvider: Provider<KeystoreManager>,
|
private val keystoreManagerProvider: Provider<KeystoreManager>,
|
||||||
) : KeystoreManager {
|
) : KeystoreManager {
|
||||||
|
|
||||||
override suspend fun get(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String): SecretKey? {
|
override suspend fun get(
|
||||||
return keystoreManagerProvider().get(masterKeyConfig, keyAlias)
|
masterKeyConfig: KeystoreManager.MasterKeyConfig,
|
||||||
|
keyAlias: String,
|
||||||
|
forceAuthentication: Boolean,
|
||||||
|
): SecretKey? {
|
||||||
|
return keystoreManagerProvider().get(masterKeyConfig, keyAlias, forceAuthentication)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(
|
override suspend fun get(
|
||||||
masterKeyConfig: KeystoreManager.MasterKeyConfig,
|
masterKeyConfig: KeystoreManager.MasterKeyConfig,
|
||||||
keyAliases: Set<String>,
|
keyAliases: Set<String>,
|
||||||
|
forceAuthentication: Boolean,
|
||||||
): Map<String, SecretKey> {
|
): Map<String, SecretKey> {
|
||||||
return keystoreManagerProvider().get(masterKeyConfig, keyAliases)
|
return keystoreManagerProvider().get(masterKeyConfig, keyAliases, forceAuthentication)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun store(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String, key: SecretKey) {
|
override suspend fun store(masterKeyConfig: KeystoreManager.MasterKeyConfig, keyAlias: String, key: SecretKey) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||||
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
|
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
|
||||||
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
|
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
|
||||||
|
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
|
||||||
import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
|
import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||||
|
|
@ -39,6 +40,7 @@ internal class MainViewModel @Inject constructor(
|
||||||
private val blockchainSDKFactory: BlockchainSDKFactory,
|
private val blockchainSDKFactory: BlockchainSDKFactory,
|
||||||
private val userWalletsListManager: UserWalletsListManager,
|
private val userWalletsListManager: UserWalletsListManager,
|
||||||
private val walletManagersFacade: WalletManagersFacade,
|
private val walletManagersFacade: WalletManagersFacade,
|
||||||
|
private val sendFeatureToggles: SendFeatureToggles,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||||
) : ViewModel(), MainIntents {
|
) : ViewModel(), MainIntents {
|
||||||
|
|
@ -61,6 +63,7 @@ internal class MainViewModel @Inject constructor(
|
||||||
viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() }
|
viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() }
|
||||||
|
|
||||||
updateAppCurrencies()
|
updateAppCurrencies()
|
||||||
|
updateSendFeatureToggle()
|
||||||
observeFlips()
|
observeFlips()
|
||||||
displayBalancesHidingStatusToast()
|
displayBalancesHidingStatusToast()
|
||||||
displayHiddenBalancesModalNotification()
|
displayHiddenBalancesModalNotification()
|
||||||
|
|
@ -108,6 +111,12 @@ internal class MainViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateSendFeatureToggle() {
|
||||||
|
viewModelScope.launch(dispatchers.main) {
|
||||||
|
sendFeatureToggles.fetchNewSendEnabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun observeFlips() {
|
private fun observeFlips() {
|
||||||
listenToFlipsUseCase().launchIn(viewModelScope)
|
listenToFlipsUseCase().launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ sealed class AnalyticsParam {
|
||||||
data object Min : FeeType("Min")
|
data object Min : FeeType("Min")
|
||||||
data object Normal : FeeType("Normal")
|
data object Normal : FeeType("Normal")
|
||||||
data object Max : FeeType("Max")
|
data object Max : FeeType("Max")
|
||||||
|
data object Custom : FeeType("Custom")
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromString(feeType: String): FeeType {
|
fun fromString(feeType: String): FeeType {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ data class PromotionInfoResponse(
|
||||||
data class BannerState(
|
data class BannerState(
|
||||||
@Json(name = "timeline") val timeline: Timeline,
|
@Json(name = "timeline") val timeline: Timeline,
|
||||||
@Json(name = "status") val status: String,
|
@Json(name = "status") val status: String,
|
||||||
|
@Json(name = "link") val link: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
|
|
||||||
|
|
@ -126,4 +126,7 @@ interface TangemTechApi {
|
||||||
@Header("card_id") cardId: String,
|
@Header("card_id") cardId: String,
|
||||||
@Path("account_id") accountId: Int,
|
@Path("account_id") accountId: Int,
|
||||||
): ApiResponse<UserTokensAccountResponse>
|
): ApiResponse<UserTokensAccountResponse>
|
||||||
|
|
||||||
|
@GET("features")
|
||||||
|
suspend fun getFeatures(): ApiResponse<FeaturesResponse>
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -61,6 +61,10 @@ object PreferencesKeys {
|
||||||
booleanPreferencesKey(name = "isTokenSwapPromoChangellyShown")
|
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 FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
|
||||||
|
|
||||||
val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") }
|
val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") }
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
|
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
|
||||||
"version": "5.9.1"
|
"version": "5.10.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "LOCAL_USER_LOGS_ENABLED",
|
"name": "LOCAL_USER_LOGS_ENABLED",
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,5 @@ dependencies {
|
||||||
implementation(deps.zxing.qrCore)
|
implementation(deps.zxing.qrCore)
|
||||||
implementation(deps.jodatime)
|
implementation(deps.jodatime)
|
||||||
implementation(deps.timber)
|
implementation(deps.timber)
|
||||||
|
implementation(deps.markdown)
|
||||||
}
|
}
|
||||||
|
|
@ -95,10 +95,14 @@ fun AmountTextField(
|
||||||
SimpleTextField(
|
SimpleTextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = { newText ->
|
onValueChange = { newText ->
|
||||||
if (decimalFormat.isValidSymbols(newText)) {
|
onValueChange(
|
||||||
val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals)
|
prepareEnter(
|
||||||
onValueChange(trimmed)
|
oldValue = value,
|
||||||
}
|
newValue = newText,
|
||||||
|
decimalFormat = decimalFormat,
|
||||||
|
decimals = decimals,
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
textStyle = textStyle.copy(
|
textStyle = textStyle.copy(
|
||||||
fontSize = fontSize,
|
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 {
|
private fun DecimalFormat.isValidSymbols(text: String): Boolean {
|
||||||
return checkDecimalSeparatorDuplicate(text) && checkGroupingSeparator(text)
|
return checkDecimalSeparatorDuplicate(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// region preview
|
// region preview
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ fun SimpleTextField(
|
||||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||||
color: Color = TangemTheme.colors.text.primary1,
|
color: Color = TangemTheme.colors.text.primary1,
|
||||||
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
|
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
|
||||||
|
placeholderColor: Color = TangemTheme.colors.text.disabled,
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
isValuePasted: Boolean = false,
|
isValuePasted: Boolean = false,
|
||||||
onValuePastedTriggerDismiss: () -> Unit = {},
|
onValuePastedTriggerDismiss: () -> Unit = {},
|
||||||
|
|
@ -108,6 +109,7 @@ fun SimpleTextField(
|
||||||
value = value,
|
value = value,
|
||||||
textStyle = textStyle,
|
textStyle = textStyle,
|
||||||
textValue = textValue,
|
textValue = textValue,
|
||||||
|
color = placeholderColor,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|
@ -122,6 +124,7 @@ private fun SimpleTextPlaceholder(
|
||||||
value: String,
|
value: String,
|
||||||
textStyle: TextStyle,
|
textStyle: TextStyle,
|
||||||
textValue: @Composable () -> Unit,
|
textValue: @Composable () -> Unit,
|
||||||
|
color: Color = TangemTheme.colors.text.disabled,
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
if (value.isBlank() && placeholder != null) {
|
if (value.isBlank() && placeholder != null) {
|
||||||
|
|
@ -132,7 +135,7 @@ private fun SimpleTextPlaceholder(
|
||||||
Text(
|
Text(
|
||||||
text = it.resolveReference(),
|
text = it.resolveReference(),
|
||||||
style = textStyle,
|
style = textStyle,
|
||||||
color = TangemTheme.colors.text.disabled,
|
color = color,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,12 +51,14 @@ fun Notification(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
containerColor: Color? = null,
|
containerColor: Color? = null,
|
||||||
iconTint: Color? = null,
|
iconTint: Color? = null,
|
||||||
|
isEnabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
BaseContainer(
|
BaseContainer(
|
||||||
buttonsState = config.buttonsState,
|
buttonsState = config.buttonsState,
|
||||||
onClick = config.onClick,
|
onClick = config.onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
|
isEnabled = isEnabled,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
|
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
|
||||||
|
|
@ -67,15 +69,16 @@ fun Notification(
|
||||||
iconTint = iconTint,
|
iconTint = iconTint,
|
||||||
title = config.title,
|
title = config.title,
|
||||||
subtitle = config.subtitle,
|
subtitle = config.subtitle,
|
||||||
isClickableComponent = config.onClick != null,
|
isClickableComponent = isEnabled && config.onClick != null,
|
||||||
)
|
)
|
||||||
|
|
||||||
Buttons(state = config.buttonsState)
|
Buttons(state = config.buttonsState, isEnabled = isEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
CloseableIconButton(
|
CloseableIconButton(
|
||||||
onClick = config.onCloseClick,
|
onClick = config.onCloseClick,
|
||||||
modifier = Modifier.align(alignment = Alignment.TopEnd),
|
modifier = Modifier.align(alignment = Alignment.TopEnd),
|
||||||
|
isEnabled = isEnabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,6 +88,7 @@ private fun BaseContainer(
|
||||||
buttonsState: NotificationConfig.ButtonsState?,
|
buttonsState: NotificationConfig.ButtonsState?,
|
||||||
onClick: (() -> Unit)?,
|
onClick: (() -> Unit)?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
isEnabled: Boolean = true,
|
||||||
containerColor: Color? = null,
|
containerColor: Color? = null,
|
||||||
content: @Composable BoxScope.() -> Unit,
|
content: @Composable BoxScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -101,7 +105,7 @@ private fun BaseContainer(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
|
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
enabled = onClick != null,
|
enabled = onClick != null && isEnabled,
|
||||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||||
color = containerColor ?: tempContainerColor,
|
color = containerColor ?: tempContainerColor,
|
||||||
) {
|
) {
|
||||||
|
|
@ -179,27 +183,31 @@ private fun TextsBlock(title: TextReference, subtitle: TextReference) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Buttons(state: NotificationButtonsState?) {
|
private fun Buttons(state: NotificationButtonsState?, isEnabled: Boolean = true) {
|
||||||
when (state) {
|
when (state) {
|
||||||
is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton(config = state)
|
is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton(
|
||||||
is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state)
|
config = state,
|
||||||
is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state)
|
isEnabled = isEnabled,
|
||||||
|
)
|
||||||
|
is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state, isEnabled = isEnabled)
|
||||||
|
is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state, isEnabled = isEnabled)
|
||||||
null -> Unit
|
null -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig) {
|
private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig, isEnabled: Boolean = true) {
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
text = config.text.resolveReference(),
|
text = config.text.resolveReference(),
|
||||||
onClick = config.onClick,
|
onClick = config.onClick,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
size = TangemButtonSize.WideAction,
|
size = TangemButtonSize.WideAction,
|
||||||
|
enabled = isEnabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig) {
|
private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig, isEnabled: Boolean = true) {
|
||||||
if (config.iconResId != null) {
|
if (config.iconResId != null) {
|
||||||
PrimaryButtonIconEnd(
|
PrimaryButtonIconEnd(
|
||||||
text = config.text.resolveReference(),
|
text = config.text.resolveReference(),
|
||||||
|
|
@ -207,6 +215,7 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo
|
||||||
onClick = config.onClick,
|
onClick = config.onClick,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
size = TangemButtonSize.WideAction,
|
size = TangemButtonSize.WideAction,
|
||||||
|
enabled = isEnabled,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
|
|
@ -214,18 +223,20 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo
|
||||||
onClick = config.onClick,
|
onClick = config.onClick,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
size = TangemButtonSize.WideAction,
|
size = TangemButtonSize.WideAction,
|
||||||
|
enabled = isEnabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) {
|
private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig, isEnabled: Boolean = true) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8)) {
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
text = config.secondaryText.resolveReference(),
|
text = config.secondaryText.resolveReference(),
|
||||||
onClick = config.onSecondaryClick,
|
onClick = config.onSecondaryClick,
|
||||||
modifier = Modifier.weight(weight = 1f),
|
modifier = Modifier.weight(weight = 1f),
|
||||||
size = TangemButtonSize.WideAction,
|
size = TangemButtonSize.WideAction,
|
||||||
|
enabled = isEnabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
|
|
@ -233,12 +244,13 @@ private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) {
|
||||||
onClick = config.onPrimaryClick,
|
onClick = config.onPrimaryClick,
|
||||||
modifier = Modifier.weight(weight = 1f),
|
modifier = Modifier.weight(weight = 1f),
|
||||||
size = TangemButtonSize.WideAction,
|
size = TangemButtonSize.WideAction,
|
||||||
|
enabled = isEnabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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) {
|
AnimatedVisibility(visible = onClick != null, modifier = modifier) {
|
||||||
onClick ?: return@AnimatedVisibility
|
onClick ?: return@AnimatedVisibility
|
||||||
|
|
||||||
|
|
@ -255,6 +267,7 @@ private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Mod
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = LocalIndication.current,
|
indication = LocalIndication.current,
|
||||||
role = Role.Button,
|
role = Role.Button,
|
||||||
|
enabled = isEnabled,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,11 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier =
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
|
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
|
||||||
.fillMaxWidth()
|
.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()
|
val (iconRef, titleRef, subtitleRef, closeIconRef, buttonRef, backgroundRef) = createRefs()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 <a href="https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=11690-12057&mode=design&t=eFnsA9sNytcQIoQ4-4">Travala Promo</a>
|
||||||
|
*/
|
||||||
|
@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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,9 @@ import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
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].
|
* 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] */
|
/** Concatenate [this] reference with [ref] */
|
||||||
operator fun TextReference.plus(ref: TextReference): TextReference {
|
operator fun TextReference.plus(ref: TextReference): TextReference {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
|
|
@ -169,4 +195,14 @@ operator fun TextReference.plus(ref: TextReference): TextReference {
|
||||||
is TextReference.Str,
|
is TextReference.Str,
|
||||||
-> TextReference.Combined(refs = wrappedList(this, ref))
|
-> 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.core.ui.utils
|
package com.tangem.core.ui.utils
|
||||||
|
|
||||||
|
import android.text.format.DateFormat
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
import org.joda.time.format.DateTimeFormat
|
import org.joda.time.format.DateTimeFormat
|
||||||
import org.joda.time.format.DateTimeFormatter
|
import org.joda.time.format.DateTimeFormatter
|
||||||
|
|
@ -55,6 +56,16 @@ object DateTimeFormatters {
|
||||||
.withLocale(Locale.getDefault())
|
.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 {
|
val dateTimeFormatter: DateTimeFormatter by lazy {
|
||||||
DateTimeFormat.forPattern("dd.MM.yyyy HH:mm")
|
DateTimeFormat.forPattern("dd.MM.yyyy HH:mm")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ import java.text.DecimalFormatSymbols
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
private const val TEXT_CHUNK_THOUSAND = 3
|
private const val TEXT_CHUNK_THOUSAND = 3
|
||||||
private const val POINT_SEPARATOR = '.'
|
|
||||||
private const val COMMA_SEPARATOR = ','
|
|
||||||
private const val SCIENTIFIC_NOTATION = 'e'
|
private const val SCIENTIFIC_NOTATION = 'e'
|
||||||
|
const val POINT_SEPARATOR = '.'
|
||||||
|
const val COMMA_SEPARATOR = ','
|
||||||
const val DECIMAL_SEPARATOR_LIMIT = 1
|
const val DECIMAL_SEPARATOR_LIMIT = 1
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 441 KiB |
|
After Width: | Height: | Size: 409 KiB |
|
|
@ -23,7 +23,17 @@ internal class DefaultPromoRepository(
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getTravalaPromoBanner(): PromoBanner? {
|
||||||
|
return runCatching(dispatchers.io) {
|
||||||
|
promoResponseConverter.convert(
|
||||||
|
tangemApi.getPromotionInfo(TRAVALA)
|
||||||
|
.getOrThrow(),
|
||||||
|
)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private const val CHANGELLY_NAME = "changelly"
|
private const val CHANGELLY_NAME = "changelly"
|
||||||
|
private const val TRAVALA = "travala"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ class PromoResponseConverter : Converter<PromotionInfoResponse, PromoBanner?> {
|
||||||
name = value.name,
|
name = value.name,
|
||||||
bannerState = PromoBanner.BannerState(
|
bannerState = PromoBanner.BannerState(
|
||||||
status = bannerState.status,
|
status = bannerState.status,
|
||||||
|
link = bannerState.link,
|
||||||
timeline = PromoBanner.Timeline(
|
timeline = PromoBanner.Timeline(
|
||||||
start = DateTime.parse(bannerState.timeline.start),
|
start = DateTime.parse(bannerState.timeline.start),
|
||||||
end = DateTime.parse(bannerState.timeline.end),
|
end = DateTime.parse(bannerState.timeline.end),
|
||||||
|
|
|
||||||
|
|
@ -3,36 +3,48 @@ package com.tangem.data.settings
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
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_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_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.get
|
||||||
import com.tangem.datasource.local.preferences.utils.store
|
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
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository for showing swap promo notification.
|
* Repository for showing swap promo notification.
|
||||||
*/
|
*/
|
||||||
class DefaultSwapPromoRepository(
|
class DefaultPromoSettingsRepository(
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
) : SwapPromoRepository {
|
) : PromoSettingsRepository {
|
||||||
override fun isReadyToShowWalletPromo(): Flow<Boolean> {
|
override fun isReadyToShowWalletSwapPromo(): Flow<Boolean> {
|
||||||
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, true)
|
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isReadyToShowTokenPromo(): Flow<Boolean> {
|
override fun isReadyToShowTokenSwapPromo(): Flow<Boolean> {
|
||||||
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY, true)
|
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setNeverToShowWalletPromo() {
|
override suspend fun setNeverToShowWalletSwapPromo() {
|
||||||
appPreferencesStore.store(
|
appPreferencesStore.store(
|
||||||
key = IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY,
|
key = IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY,
|
||||||
value = false,
|
value = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setNeverToShowTokenPromo() {
|
override suspend fun setNeverToShowTokenSwapPromo() {
|
||||||
appPreferencesStore.store(
|
appPreferencesStore.store(
|
||||||
key = IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY,
|
key = IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY,
|
||||||
value = false,
|
value = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isReadyToShowWalletTravalaPromo(): Flow<Boolean> {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,10 +2,14 @@ package com.tangem.data.settings.di
|
||||||
|
|
||||||
import com.tangem.data.settings.DefaultAppRatingRepository
|
import com.tangem.data.settings.DefaultAppRatingRepository
|
||||||
import com.tangem.data.settings.DefaultSettingsRepository
|
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.data.settings.DefaultSwapPromoRepository
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.domain.settings.repositories.AppRatingRepository
|
import com.tangem.domain.settings.repositories.AppRatingRepository
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
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 com.tangem.domain.settings.repositories.SwapPromoRepository
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -31,7 +35,7 @@ internal object SettingsDataModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideSwapPromoRepository(appPreferencesStore: AppPreferencesStore): SwapPromoRepository {
|
fun providePromoSettingsSettingsRepository(appPreferencesStore: AppPreferencesStore): PromoSettingsRepository {
|
||||||
return DefaultSwapPromoRepository(appPreferencesStore = appPreferencesStore)
|
return DefaultPromoSettingsRepository(appPreferencesStore = appPreferencesStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.tangem.domain.card
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
|
||||||
|
interface DeleteSavedAccessCodesUseCase {
|
||||||
|
|
||||||
|
suspend operator fun invoke(cardId: String): Either<Throwable, Unit>
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,12 @@ interface CardTypesResolver {
|
||||||
|
|
||||||
fun isSatoshiFriendsWallet(): Boolean
|
fun isSatoshiFriendsWallet(): Boolean
|
||||||
|
|
||||||
|
fun isBitcoinPizzaDayWallet(): Boolean
|
||||||
|
|
||||||
|
fun isVeChainWallet(): Boolean
|
||||||
|
|
||||||
|
fun isNewWorldEliteWallet(): Boolean
|
||||||
|
|
||||||
fun isWhiteWallet(): Boolean
|
fun isWhiteWallet(): Boolean
|
||||||
|
|
||||||
fun isWallet2(): Boolean
|
fun isWallet2(): Boolean
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,12 @@ internal class TangemCardTypesResolver(
|
||||||
|
|
||||||
override fun isSatoshiFriendsWallet(): Boolean = card.batchId == SATOSHI_WALLET_BATCH_ID
|
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 {
|
override fun isWhiteWallet(): Boolean {
|
||||||
return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable
|
return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable
|
||||||
}
|
}
|
||||||
|
|
@ -154,5 +160,8 @@ internal class TangemCardTypesResolver(
|
||||||
const val WHITE_WALLET2_BATCH_ID = "AF15"
|
const val WHITE_WALLET2_BATCH_ID = "AF15"
|
||||||
const val TRILLIANT_WALLET_BATCH_ID = "AF16"
|
const val TRILLIANT_WALLET_BATCH_ID = "AF16"
|
||||||
const val AVRORA_WALLET_BATCH_ID = "AF18"
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package com.tangem.domain.settings
|
package com.tangem.domain.settings
|
||||||
|
|
||||||
import com.tangem.domain.settings.repositories.SwapPromoRepository
|
import com.tangem.domain.settings.repositories.PromoSettingsRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class ShouldShowSwapPromoTokenUseCase(private val swapPromoRepository: SwapPromoRepository) {
|
class ShouldShowSwapPromoTokenUseCase(private val promoSettingsRepository: PromoSettingsRepository) {
|
||||||
|
|
||||||
operator fun invoke(): Flow<Boolean> = swapPromoRepository.isReadyToShowTokenPromo()
|
operator fun invoke(): Flow<Boolean> = promoSettingsRepository.isReadyToShowTokenSwapPromo()
|
||||||
|
|
||||||
suspend fun neverToShow() = swapPromoRepository.setNeverToShowTokenPromo()
|
suspend fun neverToShow() = promoSettingsRepository.setNeverToShowTokenSwapPromo()
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package com.tangem.domain.settings
|
package com.tangem.domain.settings
|
||||||
|
|
||||||
import com.tangem.domain.settings.repositories.SwapPromoRepository
|
import com.tangem.domain.settings.repositories.PromoSettingsRepository
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class ShouldShowSwapPromoWalletUseCase(private val swapPromoRepository: SwapPromoRepository) {
|
class ShouldShowSwapPromoWalletUseCase(private val promoSettingsRepository: PromoSettingsRepository) {
|
||||||
|
|
||||||
operator fun invoke(): Flow<Boolean> = swapPromoRepository.isReadyToShowWalletPromo()
|
operator fun invoke(): Flow<Boolean> = promoSettingsRepository.isReadyToShowWalletSwapPromo()
|
||||||
|
|
||||||
suspend fun neverToShow() = swapPromoRepository.setNeverToShowWalletPromo()
|
suspend fun neverToShow() = promoSettingsRepository.setNeverToShowWalletSwapPromo()
|
||||||
}
|
}
|
||||||
|
|
@ -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<Boolean> = promoSettingsRepository.isReadyToShowWalletTravalaPromo()
|
||||||
|
|
||||||
|
suspend fun neverToShow() = promoSettingsRepository.setNeverToShowWalletTravalaPromo()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.tangem.domain.settings.repositories
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface PromoSettingsRepository {
|
||||||
|
fun isReadyToShowWalletSwapPromo(): Flow<Boolean>
|
||||||
|
|
||||||
|
fun isReadyToShowTokenSwapPromo(): Flow<Boolean>
|
||||||
|
|
||||||
|
suspend fun setNeverToShowWalletSwapPromo()
|
||||||
|
|
||||||
|
suspend fun setNeverToShowTokenSwapPromo()
|
||||||
|
|
||||||
|
fun isReadyToShowWalletTravalaPromo(): Flow<Boolean>
|
||||||
|
|
||||||
|
suspend fun setNeverToShowWalletTravalaPromo()
|
||||||
|
}
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
package com.tangem.domain.settings.repositories
|
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface SwapPromoRepository {
|
|
||||||
fun isReadyToShowWalletPromo(): Flow<Boolean>
|
|
||||||
|
|
||||||
fun isReadyToShowTokenPromo(): Flow<Boolean>
|
|
||||||
|
|
||||||
suspend fun setNeverToShowWalletPromo()
|
|
||||||
|
|
||||||
suspend fun setNeverToShowTokenPromo()
|
|
||||||
}
|
|
||||||
|
|
@ -26,6 +26,7 @@ dependencies {
|
||||||
|
|
||||||
/** Project - Other */
|
/** Project - Other */
|
||||||
implementation(projects.core.utils)
|
implementation(projects.core.utils)
|
||||||
|
implementation(projects.libs.crypto)
|
||||||
|
|
||||||
/** Android - Other */
|
/** Android - Other */
|
||||||
implementation(deps.androidx.paging.runtime)
|
implementation(deps.androidx.paging.runtime)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ data class PromoBanner(
|
||||||
data class BannerState(
|
data class BannerState(
|
||||||
val timeline: Timeline,
|
val timeline: Timeline,
|
||||||
val status: String,
|
val status: String,
|
||||||
|
val link: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Timeline(
|
data class Timeline(
|
||||||
|
|
|
||||||
|
|
@ -45,4 +45,6 @@ sealed class CryptoCurrencyWarning {
|
||||||
val startDateTime: DateTime,
|
val startDateTime: DateTime,
|
||||||
val endDateTime: DateTime,
|
val endDateTime: DateTime,
|
||||||
) : CryptoCurrencyWarning()
|
) : CryptoCurrencyWarning()
|
||||||
|
|
||||||
|
data object BeaconChainShutdown : CryptoCurrencyWarning()
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.models.UserWalletId
|
import com.tangem.domain.wallets.models.UserWalletId
|
||||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||||
import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo
|
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.CoroutineDispatcherProvider
|
||||||
import com.tangem.utils.coroutines.runCatching
|
import com.tangem.utils.coroutines.runCatching
|
||||||
import com.tangem.utils.isNullOrZero
|
import com.tangem.utils.isNullOrZero
|
||||||
|
|
@ -74,6 +75,7 @@ class GetCurrencyWarningsUseCase(
|
||||||
*coinRelatedWarnings.toTypedArray(),
|
*coinRelatedWarnings.toTypedArray(),
|
||||||
getNetworkUnavailableWarning(currencyStatus),
|
getNetworkUnavailableWarning(currencyStatus),
|
||||||
getNetworkNoAccountWarning(currencyStatus),
|
getNetworkNoAccountWarning(currencyStatus),
|
||||||
|
getBeaconChainShutdownWarning(currency.network.id),
|
||||||
)
|
)
|
||||||
}.flowOn(dispatchers.io)
|
}.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 {
|
private fun BigDecimal?.isZero(): Boolean {
|
||||||
return this?.signum() == 0
|
return this?.signum() == 0
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<String> =
|
||||||
|
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 -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,4 +5,6 @@ import com.tangem.domain.promo.PromoBanner
|
||||||
interface PromoRepository {
|
interface PromoRepository {
|
||||||
|
|
||||||
suspend fun getChangellyPromoBanner(): PromoBanner?
|
suspend fun getChangellyPromoBanner(): PromoBanner?
|
||||||
|
|
||||||
|
suspend fun getTravalaPromoBanner(): PromoBanner?
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ package com.tangem.domain.wallets.usecase
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||||
import com.tangem.domain.wallets.models.UserWallet
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
/**
|
/**
|
||||||
* Use case for getting list of user wallets
|
* Use case for getting list of user wallets
|
||||||
*
|
*
|
||||||
|
|
@ -15,4 +15,7 @@ class GetWalletsUseCase(private val userWalletsListManager: UserWalletsListManag
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class)
|
@Throws(IllegalArgumentException::class)
|
||||||
operator fun invoke(): Flow<List<UserWallet>> = userWalletsListManager.userWallets
|
operator fun invoke(): Flow<List<UserWallet>> = userWalletsListManager.userWallets
|
||||||
|
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
suspend fun invokeSync(): List<UserWallet>? = userWalletsListManager.userWallets.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
@ -7,4 +7,7 @@ interface SendFeatureToggles {
|
||||||
|
|
||||||
/** Availability of redesigned send screen */
|
/** Availability of redesigned send screen */
|
||||||
val isRedesignedSendEnabled: Boolean
|
val isRedesignedSendEnabled: Boolean
|
||||||
|
|
||||||
|
/** Updates remote toggle */
|
||||||
|
suspend fun fetchNewSendEnabled()
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +48,7 @@ dependencies {
|
||||||
implementation(projects.core.navigation)
|
implementation(projects.core.navigation)
|
||||||
implementation(projects.core.analytics)
|
implementation(projects.core.analytics)
|
||||||
implementation(projects.core.analytics.models)
|
implementation(projects.core.analytics.models)
|
||||||
|
implementation(projects.core.datasource)
|
||||||
|
|
||||||
/** Common */
|
/** Common */
|
||||||
implementation(projects.common)
|
implementation(projects.common)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package com.tangem.features.send.impl.di
|
package com.tangem.features.send.impl.di
|
||||||
|
|
||||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
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.api.featuretoggles.SendFeatureToggles
|
||||||
import com.tangem.features.send.impl.featuretoggles.DefaultSendFeatureToggles
|
import com.tangem.features.send.impl.featuretoggles.DefaultSendFeatureToggles
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -18,7 +20,15 @@ internal object SendFeatureTogglesModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): SendFeatureToggles {
|
fun provideSendFeatureToggles(
|
||||||
return DefaultSendFeatureToggles(featureTogglesManager = featureTogglesManager)
|
featureTogglesManager: FeatureTogglesManager,
|
||||||
|
tangemTechApi: TangemTechApi,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
): SendFeatureToggles {
|
||||||
|
return DefaultSendFeatureToggles(
|
||||||
|
featureTogglesManager = featureTogglesManager,
|
||||||
|
tangemTechApi = tangemTechApi,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,41 @@
|
||||||
package com.tangem.features.send.impl.featuretoggles
|
package com.tangem.features.send.impl.featuretoggles
|
||||||
|
|
||||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
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.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
|
* Default implementation of Send feature toggles
|
||||||
*
|
*
|
||||||
* @property featureTogglesManager manager for getting information about the availability of 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(
|
internal class DefaultSendFeatureToggles(
|
||||||
private val featureTogglesManager: FeatureTogglesManager,
|
private val featureTogglesManager: FeatureTogglesManager,
|
||||||
|
private val tangemTechApi: TangemTechApi,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) : SendFeatureToggles {
|
) : SendFeatureToggles {
|
||||||
|
|
||||||
|
private val remoteSendEnabled: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||||
|
|
||||||
override val isRedesignedSendEnabled: Boolean
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package com.tangem.features.send.impl.presentation.analytics
|
package com.tangem.features.send.impl.presentation.analytics
|
||||||
|
|
||||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
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.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.SOURCE
|
||||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN
|
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN
|
||||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
|
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")
|
data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened")
|
||||||
|
|
||||||
/** Selected fee (send after next 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",
|
event = "Fee Selected",
|
||||||
params = mapOf("Fee Type" to feeType.name),
|
params = mapOf("Fee Type" to feeType.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Custom fee selected */
|
/** Custom fee selected */
|
||||||
|
|
@ -97,7 +99,16 @@ internal sealed class SendAnalyticEvents(
|
||||||
|
|
||||||
// region Transaction Result
|
// region Transaction Result
|
||||||
/** Transaction send screen opened */
|
/** 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 */
|
/** Share button clicked */
|
||||||
data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share")
|
data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share")
|
||||||
|
|
@ -145,12 +156,4 @@ internal enum class EnterAddressSource {
|
||||||
internal enum class SelectedCurrencyType(val value: String) {
|
internal enum class SelectedCurrencyType(val value: String) {
|
||||||
Token("Token"),
|
Token("Token"),
|
||||||
AppCurrency("App Currency"),
|
AppCurrency("App Currency"),
|
||||||
}
|
|
||||||
|
|
||||||
internal enum class SelectedFeeType {
|
|
||||||
Min,
|
|
||||||
Max,
|
|
||||||
Fixed,
|
|
||||||
Normal,
|
|
||||||
Custom,
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,8 +2,10 @@ package com.tangem.features.send.impl.presentation.analytics.utils
|
||||||
|
|
||||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
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.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.SendAnalyticEvents
|
||||||
import com.tangem.features.send.impl.presentation.analytics.SendScreenSource
|
import com.tangem.features.send.impl.presentation.analytics.SendScreenSource
|
||||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
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.StateRouter
|
||||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||||
|
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
|
|
||||||
internal class SendScreenAnalyticSender(
|
internal class SendScreenAnalyticSender(
|
||||||
private val stateRouterProvider: Provider<StateRouter>,
|
private val stateRouterProvider: Provider<StateRouter>,
|
||||||
private val currentStateProvider: Provider<SendUiState>,
|
private val currentStateProvider: Provider<SendUiState>,
|
||||||
|
private val cryptoCurrencyProvider: Provider<CryptoCurrency>,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
) {
|
) {
|
||||||
fun send(prevScreen: SendUiStateType, state: SendUiState) {
|
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) {
|
private fun sendSelectedFeeAnalytics(feeSelectorState: FeeSelectorState.Content) {
|
||||||
val type = when (feeSelectorState.fees) {
|
val type = getSendTransactionFeeType(feeSelectorState)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
analyticsEventHandler.send(SendAnalyticEvents.SelectedFee(type))
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
package com.tangem.features.send.impl.presentation.domain
|
package com.tangem.features.send.impl.presentation.domain
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.tangem.domain.wallets.models.UserWalletId
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Available wallet to send
|
* Available wallet to send
|
||||||
*
|
*
|
||||||
* @property name wallet name
|
* @property name wallet name
|
||||||
|
* @property userWalletId wallet id
|
||||||
* @property address blockchain address
|
* @property address blockchain address
|
||||||
*/
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
data class AvailableWallet(
|
data class AvailableWallet(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val userWalletId: UserWalletId,
|
||||||
val address: String,
|
val address: String,
|
||||||
)
|
)
|
||||||
|
|
@ -54,7 +54,7 @@ internal sealed class SendNotification(val config: NotificationConfig) {
|
||||||
wrappedList(cryptoCurrency, utxoLimit, amountLimit),
|
wrappedList(cryptoCurrency, utxoLimit, amountLimit),
|
||||||
),
|
),
|
||||||
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
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,
|
onClick = onConfirmClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -98,7 +98,7 @@ internal sealed class SendNotification(val config: NotificationConfig) {
|
||||||
title = resourceReference(R.string.send_notification_existential_deposit_title),
|
title = resourceReference(R.string.send_notification_existential_deposit_title),
|
||||||
subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)),
|
subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)),
|
||||||
buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
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,
|
onClick = onConfirmClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -119,12 +119,13 @@ internal sealed class SendNotification(val config: NotificationConfig) {
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
data class HighFeeError(
|
data class HighFeeError(
|
||||||
|
val currencyName: String,
|
||||||
val amount: String,
|
val amount: String,
|
||||||
val onConfirmClick: () -> Unit,
|
val onConfirmClick: () -> Unit,
|
||||||
val onCloseClick: () -> Unit,
|
val onCloseClick: () -> Unit,
|
||||||
) : Warning(
|
) : Warning(
|
||||||
title = resourceReference(R.string.send_notification_high_fee_title),
|
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(
|
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||||
text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)),
|
text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)),
|
||||||
onClick = onConfirmClick,
|
onClick = onConfirmClick,
|
||||||
|
|
@ -156,7 +157,7 @@ internal sealed class SendNotification(val config: NotificationConfig) {
|
||||||
data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning(
|
data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning(
|
||||||
title = resourceReference(R.string.send_network_fee_warning_title),
|
title = resourceReference(R.string.send_network_fee_warning_title),
|
||||||
subtitle = resourceReference(
|
subtitle = resourceReference(
|
||||||
R.string.send_network_fee_warning_content,
|
R.string.common_network_fee_warning_content,
|
||||||
wrappedList(cryptoAmount, fiatAmount),
|
wrappedList(cryptoAmount, fiatAmount),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,12 @@ internal class SendNotificationFactory(
|
||||||
amountValue = amountValue,
|
amountValue = amountValue,
|
||||||
feeValue = feeValue,
|
feeValue = feeValue,
|
||||||
)
|
)
|
||||||
val sendingAmount = calculateSubtractedAmount(
|
val sendingAmount = checkAndCalculateSubtractedAmount(
|
||||||
isFeeCoverage = isFeeCoverage,
|
isAmountSubtractAvailable = isFeeCoverage,
|
||||||
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
|
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
|
||||||
amountValue = amountValue,
|
amountValue = amountValue,
|
||||||
feeValue = feeValue,
|
feeValue = feeValue,
|
||||||
|
reduceAmountBy = sendState.reduceAmountBy,
|
||||||
)
|
)
|
||||||
buildList {
|
buildList {
|
||||||
// errors
|
// errors
|
||||||
|
|
@ -204,7 +205,7 @@ internal class SendNotificationFactory(
|
||||||
val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) {
|
val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) {
|
||||||
feeAmount
|
feeAmount
|
||||||
} else {
|
} else {
|
||||||
feeAmount + receivedAmount
|
receivedAmount
|
||||||
}
|
}
|
||||||
val currencyDeposit = currencyChecksRepository.getExistentialDeposit(
|
val currencyDeposit = currencyChecksRepository.getExistentialDeposit(
|
||||||
userWalletId,
|
userWalletId,
|
||||||
|
|
@ -270,6 +271,7 @@ internal class SendNotificationFactory(
|
||||||
if (!ignoreAmountReduce && isTotalBalance && isTezos) {
|
if (!ignoreAmountReduce && isTotalBalance && isTezos) {
|
||||||
add(
|
add(
|
||||||
SendNotification.Warning.HighFeeError(
|
SendNotification.Warning.HighFeeError(
|
||||||
|
currencyName = cryptoCurrencyStatus.currency.name,
|
||||||
amount = threshold.toPlainString(),
|
amount = threshold.toPlainString(),
|
||||||
onConfirmClick = {
|
onConfirmClick = {
|
||||||
clickIntents.onAmountReduceClick(
|
clickIntents.onAmountReduceClick(
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ internal fun checkFeeCoverage(
|
||||||
/**
|
/**
|
||||||
* Calculates subtracted amount
|
* Calculates subtracted amount
|
||||||
*/
|
*/
|
||||||
internal fun calculateSubtractedAmount(
|
private fun calculateSubtractedAmount(
|
||||||
isFeeCoverage: Boolean,
|
isFeeCoverage: Boolean,
|
||||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||||
amountValue: BigDecimal,
|
amountValue: BigDecimal,
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ internal class BitcoinCustomFeeConverter(
|
||||||
keyboardType = KeyboardType.Number,
|
keyboardType = KeyboardType.Number,
|
||||||
),
|
),
|
||||||
title = resourceReference(R.string.send_max_fee),
|
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(
|
label = getFiatReference(
|
||||||
rate = feeCurrency?.fiatRate,
|
rate = feeCurrency?.fiatRate,
|
||||||
value = feeValue,
|
value = feeValue,
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ internal class EthereumCustomFeeConverter(
|
||||||
keyboardType = KeyboardType.Number,
|
keyboardType = KeyboardType.Number,
|
||||||
),
|
),
|
||||||
title = resourceReference(R.string.send_max_fee),
|
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(
|
label = getFiatReference(
|
||||||
rate = feeCurrency?.fiatRate,
|
rate = feeCurrency?.fiatRate,
|
||||||
value = feeValue,
|
value = feeValue,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
import com.tangem.utils.isNullOrZero
|
import com.tangem.utils.isNullOrZero
|
||||||
|
import java.math.RoundingMode
|
||||||
|
|
||||||
internal class SendAmountFieldMaxAmountConverter(
|
internal class SendAmountFieldMaxAmountConverter(
|
||||||
private val stateRouterProvider: Provider<StateRouter>,
|
private val stateRouterProvider: Provider<StateRouter>,
|
||||||
|
|
@ -33,7 +34,7 @@ internal class SendAmountFieldMaxAmountConverter(
|
||||||
|
|
||||||
val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero()
|
val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero()
|
||||||
val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty()
|
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(
|
return state.copyWrapped(
|
||||||
isEditState = isEditState,
|
isEditState = isEditState,
|
||||||
amountState = amountState.copy(
|
amountState = amountState.copy(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
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.TangemButtonIconPosition
|
||||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||||
import com.tangem.core.ui.components.keyboardAsState
|
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.shareText
|
||||||
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
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.SendUiCurrentScreen
|
||||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||||
|
import com.tangem.features.send.impl.presentation.utils.getFiatFormatted
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SendNavigationButtons(
|
internal fun SendNavigationButtons(
|
||||||
|
|
@ -172,21 +178,27 @@ private fun SendingText(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (feeFiat != null && sendingFiat != null) {
|
if (feeFiat != null && sendingFiat != null) {
|
||||||
val sendingValue = BigDecimalFormatter.formatFiatAmount(
|
val sendingValue = getFiatFormatted(
|
||||||
fiatAmount = sendingFiat,
|
value = sendingFiat,
|
||||||
fiatCurrencyCode = feeState.appCurrency.code,
|
currencySymbol = feeState.appCurrency.symbol,
|
||||||
fiatCurrencySymbol = feeState.appCurrency.symbol,
|
currencyCode = feeState.appCurrency.code,
|
||||||
)
|
)
|
||||||
val feeValue = BigDecimalFormatter.formatFiatAmount(
|
val feeValue = getFiatFormatted(
|
||||||
fiatAmount = feeFiat,
|
value = feeState.fee?.amount?.value,
|
||||||
fiatCurrencyCode = feeState.appCurrency.code,
|
currencySymbol = feeState.appCurrency.symbol,
|
||||||
fiatCurrencySymbol = 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(
|
||||||
text = stringResource(id = R.string.send_summary_transaction_description, sendingValue, feeValue),
|
text = textResource.resolveAnnotatedReference(),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = TangemTheme.typography.caption1,
|
style = TangemTheme.typography.caption2,
|
||||||
color = TangemTheme.colors.text.tertiary,
|
color = TangemTheme.colors.text.primary1,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(TangemTheme.dimens.spacing12),
|
.padding(TangemTheme.dimens.spacing12),
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ import kotlinx.coroutines.flow.withIndex
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) {
|
internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
BackHandler(onBack = uiState.clickIntents::onBackClick)
|
val onBackClick = uiState.clickIntents::onBackClick.takeIf {
|
||||||
|
uiState.sendState?.isSending != true
|
||||||
|
} ?: {}
|
||||||
|
BackHandler(onBack = onBackClick)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ internal fun LazyListScope.notifications(
|
||||||
notifications: ImmutableList<SendNotification>,
|
notifications: ImmutableList<SendNotification>,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
hasPaddingAbove: Boolean = false,
|
hasPaddingAbove: Boolean = false,
|
||||||
|
isClickDisabled: Boolean = false,
|
||||||
) {
|
) {
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = notifications,
|
items = notifications,
|
||||||
|
|
@ -44,6 +45,7 @@ internal fun LazyListScope.notifications(
|
||||||
-> null
|
-> null
|
||||||
is SendNotification.Error -> TangemTheme.colors.icon.warning
|
is SendNotification.Error -> TangemTheme.colors.icon.warning
|
||||||
},
|
},
|
||||||
|
isEnabled = !isClickDisabled,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import com.tangem.core.ui.components.SpacerWMax
|
||||||
import com.tangem.core.ui.components.rows.SelectorRowItem
|
import com.tangem.core.ui.components.rows.SelectorRowItem
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
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.SendStates
|
||||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||||
|
|
@ -114,11 +115,14 @@ private fun FeeError(feeSelectorState: FeeSelectorState) {
|
||||||
|
|
||||||
private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? {
|
private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? {
|
||||||
val choosableFees = fees as? TransactionFee.Choosable
|
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) {
|
return when (feeType) {
|
||||||
FeeType.Slow -> choosableFees?.minimum?.amount
|
FeeType.Slow -> choosableFees?.minimum?.amount
|
||||||
FeeType.Market -> fees.normal.amount
|
FeeType.Market -> fees.normal.amount
|
||||||
FeeType.Fast -> choosableFees?.priority?.amount
|
FeeType.Fast -> choosableFees?.priority?.amount
|
||||||
FeeType.Custom -> null
|
FeeType.Custom -> customAmount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,10 @@ internal fun TextFieldWithPaste(
|
||||||
) {
|
) {
|
||||||
val (title, color) = when {
|
val (title, color) = when {
|
||||||
isError && error != null -> error to TangemTheme.colors.text.warning
|
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
|
else -> label to TangemTheme.colors.text.secondary
|
||||||
}
|
}
|
||||||
|
val placeholderColor = if (isReadOnly) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.disabled
|
||||||
FooterContainer(modifier, footer) {
|
FooterContainer(modifier, footer) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -59,6 +60,7 @@ internal fun TextFieldWithPaste(
|
||||||
SimpleTextField(
|
SimpleTextField(
|
||||||
value = value,
|
value = value,
|
||||||
placeholder = placeholder,
|
placeholder = placeholder,
|
||||||
|
placeholderColor = placeholderColor,
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
readOnly = isReadOnly,
|
readOnly = isReadOnly,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import com.tangem.features.send.impl.presentation.state.previewdata.AmountStateP
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AmountBlock(
|
internal fun AmountBlock(
|
||||||
amountState: SendStates.AmountState,
|
amountState: SendStates.AmountState,
|
||||||
isSuccess: Boolean,
|
isClickDisabled: Boolean,
|
||||||
isEditingDisabled: Boolean,
|
isEditingDisabled: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -55,7 +55,7 @@ internal fun AmountBlock(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick)
|
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
|
||||||
.padding(
|
.padding(
|
||||||
vertical = TangemTheme.dimens.spacing14,
|
vertical = TangemTheme.dimens.spacing14,
|
||||||
horizontal = TangemTheme.dimens.spacing16,
|
horizontal = TangemTheme.dimens.spacing16,
|
||||||
|
|
@ -92,7 +92,7 @@ private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::cla
|
||||||
TangemThemePreview {
|
TangemThemePreview {
|
||||||
AmountBlock(
|
AmountBlock(
|
||||||
amountState = value,
|
amountState = value,
|
||||||
isSuccess = false,
|
isClickDisabled = false,
|
||||||
isEditingDisabled = false,
|
isEditingDisabled = false,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,13 @@ import com.tangem.features.send.impl.presentation.utils.getCryptoReference
|
||||||
import com.tangem.features.send.impl.presentation.utils.getFiatReference
|
import com.tangem.features.send.impl.presentation.utils.getFiatReference
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) {
|
internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, onClick: () -> Unit) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||||
.background(TangemTheme.colors.background.action)
|
.background(TangemTheme.colors.background.action)
|
||||||
.clickable(enabled = !isSuccess, onClick = onClick)
|
.clickable(enabled = !isClickDisabled, onClick = onClick)
|
||||||
.padding(TangemTheme.dimens.spacing12),
|
.padding(TangemTheme.dimens.spacing12),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -118,7 +118,7 @@ private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) va
|
||||||
TangemThemePreview {
|
TangemThemePreview {
|
||||||
FeeBlock(
|
FeeBlock(
|
||||||
feeState = value,
|
feeState = value,
|
||||||
isSuccess = true,
|
isClickDisabled = true,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import com.tangem.features.send.impl.presentation.state.previewdata.RecipientSta
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RecipientBlock(
|
internal fun RecipientBlock(
|
||||||
recipientState: SendStates.RecipientState,
|
recipientState: SendStates.RecipientState,
|
||||||
isSuccess: Boolean,
|
isClickDisabled: Boolean,
|
||||||
isEditingDisabled: Boolean,
|
isEditingDisabled: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -40,7 +40,7 @@ internal fun RecipientBlock(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick)
|
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
|
||||||
.padding(TangemTheme.dimens.spacing12),
|
.padding(TangemTheme.dimens.spacing12),
|
||||||
) {
|
) {
|
||||||
AddressBlock(recipientState.addressTextField)
|
AddressBlock(recipientState.addressTextField)
|
||||||
|
|
@ -107,7 +107,7 @@ private fun RecipientBlockPreview(
|
||||||
TangemThemePreview {
|
TangemThemePreview {
|
||||||
RecipientBlock(
|
RecipientBlock(
|
||||||
recipientState = value,
|
recipientState = value,
|
||||||
isSuccess = true,
|
isClickDisabled = true,
|
||||||
isEditingDisabled = false,
|
isEditingDisabled = false,
|
||||||
onClick = {},
|
onClick = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,13 @@ private const val TAP_HELP_ANIMATION_DELAY = 500L
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SendContent(uiState: SendUiState) {
|
internal fun SendContent(uiState: SendUiState) {
|
||||||
val sendState = uiState.sendState ?: return
|
val sendState = uiState.sendState ?: return
|
||||||
|
val isClickDisabled = sendState.isSending || sendState.isSuccess
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||||
) {
|
) {
|
||||||
blocks(uiState)
|
blocks(uiState)
|
||||||
tapHelp(isDisplay = sendState.showTapHelp)
|
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 feeState = uiState.feeState ?: return
|
||||||
val sendState = uiState.sendState ?: return
|
val sendState = uiState.sendState ?: return
|
||||||
val isSuccess = sendState.isSuccess
|
val isSuccess = sendState.isSuccess
|
||||||
|
val isClickDisabled = sendState.isSending || isSuccess
|
||||||
val timestamp = sendState.transactionDate
|
val timestamp = sendState.transactionDate
|
||||||
|
|
||||||
item(key = BLOCKS_KEY) {
|
item(key = BLOCKS_KEY) {
|
||||||
|
|
@ -65,19 +67,19 @@ private fun LazyListScope.blocks(uiState: SendUiState) {
|
||||||
}
|
}
|
||||||
RecipientBlock(
|
RecipientBlock(
|
||||||
recipientState = recipientState,
|
recipientState = recipientState,
|
||||||
isSuccess = isSuccess,
|
isClickDisabled = isClickDisabled,
|
||||||
isEditingDisabled = uiState.isEditingDisabled,
|
isEditingDisabled = uiState.isEditingDisabled,
|
||||||
onClick = uiState.clickIntents::showRecipient,
|
onClick = uiState.clickIntents::showRecipient,
|
||||||
)
|
)
|
||||||
AmountBlock(
|
AmountBlock(
|
||||||
amountState = amountState,
|
amountState = amountState,
|
||||||
isSuccess = isSuccess,
|
isClickDisabled = isClickDisabled,
|
||||||
isEditingDisabled = uiState.isEditingDisabled,
|
isEditingDisabled = uiState.isEditingDisabled,
|
||||||
onClick = uiState.clickIntents::showAmount,
|
onClick = uiState.clickIntents::showAmount,
|
||||||
)
|
)
|
||||||
FeeBlock(
|
FeeBlock(
|
||||||
feeState = feeState,
|
feeState = feeState,
|
||||||
isSuccess = isSuccess,
|
isClickDisabled = isClickDisabled,
|
||||||
onClick = uiState.clickIntents::showFee,
|
onClick = uiState.clickIntents::showFee,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import java.math.BigDecimal
|
||||||
import java.math.RoundingMode
|
import java.math.RoundingMode
|
||||||
|
|
||||||
private const val FIAT_DECIMALS = 2
|
private const val FIAT_DECIMALS = 2
|
||||||
|
private const val CRYPTO_FEE_DECIMALS = 6
|
||||||
private const val FEE_MINIMUM_VALUE = 0.01
|
private const val FEE_MINIMUM_VALUE = 0.01
|
||||||
|
|
||||||
internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? {
|
internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? {
|
||||||
|
|
@ -21,7 +22,7 @@ internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): Tex
|
||||||
BigDecimalFormatter.formatCryptoAmount(
|
BigDecimalFormatter.formatCryptoAmount(
|
||||||
cryptoAmount = amount.value,
|
cryptoAmount = amount.value,
|
||||||
cryptoCurrency = amount.currencySymbol,
|
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 {
|
internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String {
|
||||||
if (value == null || rate == null) return EMPTY_BALANCE_SIGN
|
if (value == null || rate == null) return EMPTY_BALANCE_SIGN
|
||||||
val feeValue = value.multiply(rate)
|
val feeValue = value.multiply(rate)
|
||||||
val scaled = feeValue.setScale(FIAT_DECIMALS, RoundingMode.UP) ?: BigDecimal.ZERO
|
return getFiatFormatted(feeValue, appCurrency.code, appCurrency.symbol)
|
||||||
val formattedValue = if (scaled < BigDecimal(FEE_MINIMUM_VALUE)) {
|
}
|
||||||
|
|
||||||
|
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 {
|
buildString {
|
||||||
append(BigDecimalFormatter.CAN_BE_LOWER_SIGN)
|
append(BigDecimalFormatter.CAN_BE_LOWER_SIGN)
|
||||||
append(
|
append(
|
||||||
BigDecimalFormatter.formatFiatAmount(
|
BigDecimalFormatter.formatFiatAmount(
|
||||||
fiatAmount = BigDecimal(FEE_MINIMUM_VALUE),
|
fiatAmount = BigDecimal(FEE_MINIMUM_VALUE),
|
||||||
fiatCurrencyCode = appCurrency.code,
|
fiatCurrencyCode = currencyCode,
|
||||||
fiatCurrencySymbol = appCurrency.symbol,
|
fiatCurrencySymbol = currencySymbol,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
BigDecimalFormatter.formatFiatAmount(
|
BigDecimalFormatter.formatFiatAmount(
|
||||||
fiatAmount = feeValue,
|
fiatAmount = value,
|
||||||
fiatCurrencyCode = appCurrency.code,
|
fiatCurrencyCode = currencyCode,
|
||||||
fiatCurrencySymbol = appCurrency.symbol,
|
fiatCurrencySymbol = currencySymbol,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return formattedValue
|
|
||||||
}
|
}
|
||||||
|
|
@ -26,7 +26,6 @@ import com.tangem.domain.tokens.*
|
||||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
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.repository.CurrencyChecksRepository
|
||||||
import com.tangem.domain.tokens.utils.convertToAmount
|
import com.tangem.domain.tokens.utils.convertToAmount
|
||||||
import com.tangem.domain.transaction.error.GetFeeError
|
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.JobHolder
|
||||||
import com.tangem.utils.coroutines.saveIn
|
import com.tangem.utils.coroutines.saveIn
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
@ -76,8 +76,8 @@ internal class SendViewModel @Inject constructor(
|
||||||
private val getWalletsUseCase: GetWalletsUseCase,
|
private val getWalletsUseCase: GetWalletsUseCase,
|
||||||
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
|
||||||
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
|
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||||
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
|
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
||||||
private val getCryptoCurrencyStatusesSyncUseCase: GetCryptoCurrencyStatusesSyncUseCase,
|
private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
|
||||||
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
|
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
|
||||||
private val getFeeUseCase: GetFeeUseCase,
|
private val getFeeUseCase: GetFeeUseCase,
|
||||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||||
|
|
@ -92,6 +92,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase,
|
private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase,
|
||||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||||
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
|
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
|
||||||
|
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||||
validateTransactionUseCase: ValidateTransactionUseCase,
|
validateTransactionUseCase: ValidateTransactionUseCase,
|
||||||
currencyChecksRepository: CurrencyChecksRepository,
|
currencyChecksRepository: CurrencyChecksRepository,
|
||||||
isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||||
|
|
@ -179,6 +180,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
stateRouterProvider = Provider { stateRouter },
|
stateRouterProvider = Provider { stateRouter },
|
||||||
currentStateProvider = Provider { uiState },
|
currentStateProvider = Provider { uiState },
|
||||||
analyticsEventHandler = analyticsEventHandler,
|
analyticsEventHandler = analyticsEventHandler,
|
||||||
|
cryptoCurrencyProvider = Provider { cryptoCurrency },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,6 +189,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var userWallet: UserWallet by Delegates.notNull()
|
private var userWallet: UserWallet by Delegates.notNull()
|
||||||
|
private var userWallets: List<AvailableWallet> = emptyList()
|
||||||
private var isAmountSubtractAvailable: Boolean = false
|
private var isAmountSubtractAvailable: Boolean = false
|
||||||
private var isTapHelpPreviewEnabled: Boolean = false
|
private var isTapHelpPreviewEnabled: Boolean = false
|
||||||
private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||||
|
|
@ -399,57 +402,48 @@ internal class SendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUserWallets() {
|
private fun getUserWallets() {
|
||||||
getWalletsUseCase()
|
viewModelScope.launch(dispatchers.main) {
|
||||||
.conflate()
|
runCatching {
|
||||||
.distinctUntilChanged()
|
getWalletsUseCase.invokeSync()
|
||||||
.onEach { userWallets ->
|
?.toAvailableWallets()
|
||||||
coroutineScope {
|
.orEmpty()
|
||||||
runCatching {
|
}.onSuccess { result ->
|
||||||
userWallets
|
combine(*result.toTypedArray()) { it }
|
||||||
.filterNot { it.walletId == userWalletId || it.isLocked }
|
.onEach {
|
||||||
.map { wallet ->
|
userWallets = it.filterNotNull().toList()
|
||||||
async(dispatchers.io) { wallet.toAvailableWallet() }
|
uiState = stateFactory.onLoadedWalletsList(wallets = userWallets)
|
||||||
}.awaitAll()
|
|
||||||
}.onSuccess { result ->
|
|
||||||
uiState = stateFactory.onLoadedWalletsList(wallets = result)
|
|
||||||
}.onFailure {
|
|
||||||
uiState = stateFactory.onLoadedWalletsList(wallets = emptyList())
|
|
||||||
}
|
}
|
||||||
}
|
.flowOn(dispatchers.main)
|
||||||
}
|
.launchIn(viewModelScope)
|
||||||
.flowOn(dispatchers.main)
|
}.onFailure {
|
||||||
.launchIn(viewModelScope)
|
uiState = stateFactory.onLoadedWalletsList(wallets = emptyList())
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun List<UserWallet>.toAvailableWallets(): List<Flow<AvailableWallet?>> =
|
||||||
|
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() {
|
private suspend fun getTxHistory() {
|
||||||
val txHistoryList = getFixedTxHistoryItemsUseCase.getSync(
|
val txHistoryList = getFixedTxHistoryItemsUseCase.getSync(
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
|
|
@ -874,11 +868,25 @@ internal class SendViewModel @Inject constructor(
|
||||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||||
updateTransactionStatus(txData)
|
updateTransactionStatus(txData)
|
||||||
scheduleBalanceUpdate()
|
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) {
|
private suspend fun updateTransactionStatus(txData: TransactionData) {
|
||||||
val txUrl = getExplorerTransactionUrlUseCase(
|
val txUrl = getExplorerTransactionUrlUseCase(
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,7 @@ internal class StateBuilder(
|
||||||
val warnings = getWarningsForSuccessState(
|
val warnings = getWarningsForSuccessState(
|
||||||
quoteModel = quoteModel,
|
quoteModel = quoteModel,
|
||||||
fromToken = fromToken,
|
fromToken = fromToken,
|
||||||
|
selectedFeeType = selectedFeeType,
|
||||||
)
|
)
|
||||||
val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
|
val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
|
||||||
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
|
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
|
||||||
|
|
@ -314,12 +315,14 @@ internal class StateBuilder(
|
||||||
private fun getWarningsForSuccessState(
|
private fun getWarningsForSuccessState(
|
||||||
quoteModel: SwapState.QuotesLoadedState,
|
quoteModel: SwapState.QuotesLoadedState,
|
||||||
fromToken: CryptoCurrency,
|
fromToken: CryptoCurrency,
|
||||||
|
ignoreAmountReduce: Boolean,
|
||||||
|
selectedFeeType: FeeType,
|
||||||
): List<SwapWarning> {
|
): List<SwapWarning> {
|
||||||
val warnings = mutableListOf<SwapWarning>()
|
val warnings = mutableListOf<SwapWarning>()
|
||||||
maybeAddDomainWarnings(quoteModel, warnings)
|
maybeAddDomainWarnings(quoteModel, warnings)
|
||||||
maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings)
|
maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings)
|
||||||
maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken)
|
maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken)
|
||||||
maybeAddNetworkFeeCoverageWarning(quoteModel, warnings)
|
maybeAddNetworkFeeCoverageWarning(quoteModel, warnings, selectedFeeType)
|
||||||
maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings)
|
maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings)
|
||||||
maybeAddInsufficientFundsWarning(quoteModel, warnings)
|
maybeAddInsufficientFundsWarning(quoteModel, warnings)
|
||||||
maybeAddTransactionInProgressWarning(quoteModel, warnings)
|
maybeAddTransactionInProgressWarning(quoteModel, warnings)
|
||||||
|
|
@ -460,18 +463,37 @@ internal class StateBuilder(
|
||||||
private fun maybeAddNetworkFeeCoverageWarning(
|
private fun maybeAddNetworkFeeCoverageWarning(
|
||||||
quoteModel: SwapState.QuotesLoadedState,
|
quoteModel: SwapState.QuotesLoadedState,
|
||||||
warnings: MutableList<SwapWarning>,
|
warnings: MutableList<SwapWarning>,
|
||||||
|
selectedFeeType: FeeType,
|
||||||
) {
|
) {
|
||||||
when (quoteModel.preparedSwapConfigState.includeFeeInAmount) {
|
when (quoteModel.preparedSwapConfigState.includeFeeInAmount) {
|
||||||
is IncludeFeeInAmount.Included ->
|
is IncludeFeeInAmount.Included -> {
|
||||||
|
val fee = selectFeeByType(selectedFeeType, quoteModel.txFee) ?: return
|
||||||
warnings.add(
|
warnings.add(
|
||||||
SwapWarning.GeneralWarning(
|
SwapWarning.GeneralWarning(
|
||||||
createNetworkFeeCoverageNotificationConfig(),
|
createNetworkFeeCoverageNotificationConfig(
|
||||||
|
quoteModel.fromTokenInfo.tokenAmount.getFormattedCryptoAmount(
|
||||||
|
quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency,
|
||||||
|
),
|
||||||
|
fee.feeFiatFormatted,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
else -> Unit
|
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(
|
private fun maybeAddUnableCoverFeeWarning(
|
||||||
quoteModel: SwapState.QuotesLoadedState,
|
quoteModel: SwapState.QuotesLoadedState,
|
||||||
fromToken: CryptoCurrency,
|
fromToken: CryptoCurrency,
|
||||||
|
|
@ -547,12 +569,12 @@ internal class StateBuilder(
|
||||||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||||
val warnings = mutableListOf<SwapWarning>()
|
val warnings = mutableListOf<SwapWarning>()
|
||||||
warnings.add(getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency))
|
warnings.add(getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency))
|
||||||
if (includeFeeInAmount is IncludeFeeInAmount.Included) {
|
if (includeFeeInAmount is IncludeFeeInAmount.Included && uiStateHolder.fee is FeeItemState.Content) {
|
||||||
warnings.add(
|
val feeCoverageNotification = createNetworkFeeCoverageNotificationConfig(
|
||||||
SwapWarning.GeneralWarning(
|
fromToken.tokenAmount.getFormattedCryptoAmount(fromToken.cryptoCurrencyStatus.currency),
|
||||||
createNetworkFeeCoverageNotificationConfig(),
|
uiStateHolder.fee.amountFiatFormatted,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
warnings.add(SwapWarning.GeneralWarning(feeCoverageNotification))
|
||||||
}
|
}
|
||||||
val providerState = getProviderStateForError(
|
val providerState = getProviderStateForError(
|
||||||
swapProvider = swapProvider,
|
swapProvider = swapProvider,
|
||||||
|
|
@ -1402,10 +1424,16 @@ internal class StateBuilder(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNetworkFeeCoverageNotificationConfig(): NotificationConfig {
|
private fun createNetworkFeeCoverageNotificationConfig(
|
||||||
|
cryptoAmount: String,
|
||||||
|
fiatAmount: String,
|
||||||
|
): NotificationConfig {
|
||||||
return NotificationConfig(
|
return NotificationConfig(
|
||||||
title = resourceReference(R.string.send_network_fee_warning_title),
|
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,
|
iconResId = R.drawable.img_attention_20,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ internal class TokenDetailsNotificationsAnalyticsSender(
|
||||||
is TokenDetailsNotification.TopUpWithoutReserve,
|
is TokenDetailsNotification.TopUpWithoutReserve,
|
||||||
is TokenDetailsNotification.RentInfo,
|
is TokenDetailsNotification.RentInfo,
|
||||||
is TokenDetailsNotification.SwapPromo,
|
is TokenDetailsNotification.SwapPromo,
|
||||||
|
is TokenDetailsNotification.NetworkShutdown,
|
||||||
-> null
|
-> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
title = resourceReference(R.string.warning_no_account_title),
|
||||||
subtitle = resourceReference(
|
subtitle = resourceReference(
|
||||||
id = R.string.no_account_generic,
|
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),
|
title = resourceReference(id = R.string.warning_no_account_title),
|
||||||
subtitle = resourceReference(id = R.string.no_account_send_to_create),
|
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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -3,12 +3,15 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
|
||||||
import com.tangem.blockchain.common.Blockchain
|
import com.tangem.blockchain.common.Blockchain
|
||||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
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.CryptoCurrency
|
||||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
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.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.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.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||||
|
import com.tangem.features.tokendetails.impl.R
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
import com.tangem.utils.extensions.removeBy
|
import com.tangem.utils.extensions.removeBy
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -80,6 +83,10 @@ internal class TokenDetailsNotificationConverter(
|
||||||
onSwapClick = clickIntents::onSwapPromoClick,
|
onSwapClick = clickIntents::onSwapPromoClick,
|
||||||
onCloseClick = clickIntents::onSwapPromoDismiss,
|
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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,4 +121,38 @@ sealed class WalletScreenAnalyticsEvent {
|
||||||
|
|
||||||
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
|
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class Promotion(
|
||||||
|
event: String,
|
||||||
|
params: Map<String, String> = 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
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.analytics.WalletScreenAnalyticsEvent.MainScreen
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
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.Informational.MissingAddresses -> MainScreen.MissingAddresses
|
||||||
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
|
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
|
||||||
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
|
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.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
|
||||||
is WalletNotification.Informational.NoAccount,
|
is WalletNotification.Informational.NoAccount,
|
||||||
is WalletNotification.Warning.LowSignatures,
|
is WalletNotification.Warning.LowSignatures,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import com.tangem.domain.common.util.cardTypesResolver
|
||||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||||
import com.tangem.domain.promo.PromoBanner
|
import com.tangem.domain.promo.PromoBanner
|
||||||
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
|
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.GetTokenListUseCase
|
||||||
import com.tangem.domain.tokens.error.TokenListError
|
import com.tangem.domain.tokens.error.TokenListError
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
|
|
@ -34,7 +34,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
private val getTokenListUseCase: GetTokenListUseCase,
|
private val getTokenListUseCase: GetTokenListUseCase,
|
||||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||||
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
|
private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase,
|
||||||
private val promoRepository: PromoRepository,
|
private val promoRepository: PromoRepository,
|
||||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||||
private val backupValidator: BackupValidator,
|
private val backupValidator: BackupValidator,
|
||||||
|
|
@ -45,18 +45,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||||
|
|
||||||
val promoFlow = flow { emit(promoRepository.getChangellyPromoBanner()) }
|
val travalaPromoFlow = flow { emit(promoRepository.getTravalaPromoBanner()) }
|
||||||
return combine(
|
return combine(
|
||||||
flow = getTokenListUseCase.launch(userWallet.walletId).conflate(),
|
flow = getTokenListUseCase.launch(userWallet.walletId).conflate(),
|
||||||
flow2 = isReadyToShowRateAppUseCase().conflate(),
|
flow2 = isReadyToShowRateAppUseCase().conflate(),
|
||||||
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
|
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
|
||||||
flow4 = shouldShowSwapPromoWalletUseCase().conflate(),
|
flow4 = shouldShowTravalaPromoWalletUseCase().conflate(),
|
||||||
flow5 = promoFlow,
|
flow5 = travalaPromoFlow.conflate(),
|
||||||
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner ->
|
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowTravalaPromo, promoBanner ->
|
||||||
|
|
||||||
readyForRateAppNotification = true
|
readyForRateAppNotification = true
|
||||||
buildList {
|
buildList {
|
||||||
addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents)
|
addTravalaPromoNotification(shouldShowTravalaPromo, promoBanner, clickIntents)
|
||||||
|
|
||||||
addCriticalNotifications(userWallet)
|
addCriticalNotifications(userWallet)
|
||||||
|
|
||||||
|
|
@ -69,16 +69,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableList<WalletNotification>.addSwapPromoNotification(
|
private fun MutableList<WalletNotification>.addTravalaPromoNotification(
|
||||||
shouldShowPromo: Boolean,
|
shouldShowPromo: Boolean,
|
||||||
promoBanner: PromoBanner?,
|
promoBanner: PromoBanner?,
|
||||||
clickIntents: WalletClickIntents,
|
clickIntents: WalletClickIntents,
|
||||||
) {
|
) {
|
||||||
promoBanner ?: return
|
promoBanner ?: return
|
||||||
val promoNotification = WalletNotification.SwapPromo(
|
val promoNotification = WalletNotification.TravalaPromo(
|
||||||
startDateTime = promoBanner.bannerState.timeline.start,
|
startDateTime = promoBanner.bannerState.timeline.start,
|
||||||
endDateTime = promoBanner.bannerState.timeline.end,
|
endDateTime = promoBanner.bannerState.timeline.end,
|
||||||
onCloseClick = clickIntents::onCloseSwapPromoClick,
|
bannerLink = promoBanner.bannerState.link,
|
||||||
|
onBookNowButtonClick = clickIntents::onTravalaPromoClick,
|
||||||
|
onCloseClick = clickIntents::onCloseTravalaPromoClick,
|
||||||
)
|
)
|
||||||
addIf(
|
addIf(
|
||||||
element = promoNotification,
|
element = promoNotification,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ internal object WalletImageResolver {
|
||||||
cardTypesResolver.isJrWallet() -> userWallet.resolveJrWallet()
|
cardTypesResolver.isJrWallet() -> userWallet.resolveJrWallet()
|
||||||
cardTypesResolver.isGrimWallet() -> userWallet.resolveGrimWallet()
|
cardTypesResolver.isGrimWallet() -> userWallet.resolveGrimWallet()
|
||||||
cardTypesResolver.isSatoshiFriendsWallet() -> userWallet.resolveSatoshiWallet()
|
cardTypesResolver.isSatoshiFriendsWallet() -> userWallet.resolveSatoshiWallet()
|
||||||
|
cardTypesResolver.isBitcoinPizzaDayWallet() -> userWallet.resolveBitcoinPizzaDayWallet()
|
||||||
|
cardTypesResolver.isVeChainWallet() -> userWallet.resolveVeChainWallet()
|
||||||
|
cardTypesResolver.isNewWorldEliteWallet() -> userWallet.resolveNewWorldEliteWallet()
|
||||||
cardTypesResolver.isWallet2() -> userWallet.resolveWallet2()
|
cardTypesResolver.isWallet2() -> userWallet.resolveWallet2()
|
||||||
cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet()
|
cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet()
|
||||||
cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1()
|
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? {
|
private fun UserWallet.resolveWallet1(): Int? {
|
||||||
return resolveWalletWithBackups { count ->
|
return resolveWalletWithBackups { count ->
|
||||||
when (count) {
|
when (count) {
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
import com.tangem.core.ui.extensions.*
|
||||||
import com.tangem.core.ui.extensions.pluralReference
|
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
|
||||||
import com.tangem.core.ui.extensions.wrappedList
|
|
||||||
import com.tangem.feature.wallet.impl.R
|
import com.tangem.feature.wallet.impl.R
|
||||||
import org.joda.time.DateTime
|
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(
|
data class SwapPromo(
|
||||||
val startDateTime: DateTime,
|
val startDateTime: DateTime,
|
||||||
val endDateTime: DateTime,
|
val endDateTime: DateTime,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.tangem.core.ui.components.notifications.Notification
|
import com.tangem.core.ui.components.notifications.Notification
|
||||||
import com.tangem.core.ui.components.notifications.NotificationWithBackground
|
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.core.ui.res.TangemTheme
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
@ -25,23 +26,33 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
|
||||||
key = { it::class.java },
|
key = { it::class.java },
|
||||||
contentType = { it::class.java },
|
contentType = { it::class.java },
|
||||||
itemContent = {
|
itemContent = {
|
||||||
if (it is WalletNotification.SwapPromo) {
|
// TODO develop promo banner general component
|
||||||
NotificationWithBackground(
|
when (it) {
|
||||||
config = it.config,
|
is WalletNotification.SwapPromo -> {
|
||||||
modifier = modifier.animateItemPlacement(),
|
NotificationWithBackground(
|
||||||
)
|
config = it.config,
|
||||||
} else {
|
modifier = modifier.animateItemPlacement(),
|
||||||
Notification(
|
)
|
||||||
config = it.config,
|
}
|
||||||
modifier = modifier.animateItemPlacement(),
|
is WalletNotification.TravalaPromo -> {
|
||||||
iconTint = when (it) {
|
TravalaNotificationWithBackground(
|
||||||
is WalletNotification.Critical -> TangemTheme.colors.icon.warning
|
config = it.config,
|
||||||
is WalletNotification.Informational -> TangemTheme.colors.icon.accent
|
modifier = modifier.animateItemPlacement(),
|
||||||
is WalletNotification.RateApp -> TangemTheme.colors.icon.attention
|
)
|
||||||
is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1
|
}
|
||||||
else -> null
|
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
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
||||||
|
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
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.models.UserWalletId
|
||||||
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
|
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.domain.wallets.usecase.UpdateWalletUseCase
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
||||||
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
|
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
|
||||||
|
|
@ -32,9 +36,13 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
|
||||||
private val stateHolder: WalletStateController,
|
private val stateHolder: WalletStateController,
|
||||||
private val walletEventSender: WalletEventSender,
|
private val walletEventSender: WalletEventSender,
|
||||||
private val walletScreenContentLoader: WalletScreenContentLoader,
|
private val walletScreenContentLoader: WalletScreenContentLoader,
|
||||||
|
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||||
|
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||||
private val updateWalletUseCase: UpdateWalletUseCase,
|
private val updateWalletUseCase: UpdateWalletUseCase,
|
||||||
private val deleteWalletUseCase: DeleteWalletUseCase,
|
private val deleteWalletUseCase: DeleteWalletUseCase,
|
||||||
|
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
|
private val reduxStateHolder: ReduxStateHolder,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) : BaseWalletClickIntents(), WalletCardClickIntents {
|
) : BaseWalletClickIntents(), WalletCardClickIntents {
|
||||||
|
|
||||||
|
|
@ -72,7 +80,18 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
|
||||||
override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) {
|
override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) {
|
||||||
viewModelScope.launch(dispatchers.main) {
|
viewModelScope.launch(dispatchers.main) {
|
||||||
walletScreenContentLoader.cancel(userWalletId)
|
walletScreenContentLoader.cancel(userWalletId)
|
||||||
|
|
||||||
|
val deletedUserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch
|
||||||
|
|
||||||
|
deleteSavedAccessCodesUseCase(cardId = deletedUserWallet.cardId)
|
||||||
|
.onLeft { Timber.e(it.toString()) }
|
||||||
|
|
||||||
deleteWalletUseCase(userWalletId)
|
deleteWalletUseCase(userWalletId)
|
||||||
|
.onRight {
|
||||||
|
getSelectedWalletSyncUseCase().getOrNull()?.let {
|
||||||
|
reduxStateHolder.onUserWalletSelected(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
.onLeft { Timber.e(it.toString()) }
|
.onLeft { Timber.e(it.toString()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.tangem.domain.redux.ReduxStateHolder
|
||||||
import com.tangem.domain.settings.NeverToSuggestRateAppUseCase
|
import com.tangem.domain.settings.NeverToSuggestRateAppUseCase
|
||||||
import com.tangem.domain.settings.RemindToRateAppLaterUseCase
|
import com.tangem.domain.settings.RemindToRateAppLaterUseCase
|
||||||
import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
|
import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
|
||||||
|
import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase
|
||||||
import com.tangem.domain.tokens.FetchTokenListUseCase
|
import com.tangem.domain.tokens.FetchTokenListUseCase
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
|
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.GetUserWalletUseCase
|
||||||
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
|
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
|
||||||
import com.tangem.feature.wallet.impl.R
|
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.Basic
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler
|
import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler
|
||||||
|
|
@ -56,6 +58,10 @@ internal interface WalletWarningsClickIntents {
|
||||||
fun onCloseRateAppWarningClick()
|
fun onCloseRateAppWarningClick()
|
||||||
|
|
||||||
fun onCloseSwapPromoClick()
|
fun onCloseSwapPromoClick()
|
||||||
|
|
||||||
|
fun onTravalaPromoClick(link: String?)
|
||||||
|
|
||||||
|
fun onCloseTravalaPromoClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
|
|
@ -75,6 +81,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
||||||
private val reduxStateHolder: ReduxStateHolder,
|
private val reduxStateHolder: ReduxStateHolder,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
|
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
|
||||||
|
private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase,
|
||||||
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
|
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
|
||||||
|
|
||||||
override fun onAddBackupCardClick() {
|
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? {
|
private suspend fun getSelectedUserWallet(): UserWallet? {
|
||||||
val userWalletId = stateHolder.getSelectedWalletId()
|
val userWalletId = stateHolder.getSelectedWalletId()
|
||||||
return getUserWalletUseCase(userWalletId).getOrElse {
|
return getUserWalletUseCase(userWalletId).getOrElse {
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
|
@ -83,6 +83,7 @@ web3j = "4.10.1"
|
||||||
leakcanary = "2.13"
|
leakcanary = "2.13"
|
||||||
decompose = "2.2.2"
|
decompose = "2.2.2"
|
||||||
room = "2.6.1"
|
room = "2.6.1"
|
||||||
|
markdown = "0.7.2"
|
||||||
# endregion Other libraries
|
# endregion Other libraries
|
||||||
|
|
||||||
# region Tangem
|
# 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-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||||
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||||
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
|
||||||
|
markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" }
|
||||||
# endregion Other
|
# endregion Other
|
||||||
|
|
|
||||||
|
|
@ -49,4 +49,10 @@ object BlockchainUtils {
|
||||||
val blockchain = Blockchain.fromId(networkId)
|
val blockchain = Blockchain.fromId(networkId)
|
||||||
return blockchain == Blockchain.Cardano
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||