diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/MarketsScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/MarketsScenarios.kt index 9c3b33b013..cda67fda49 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/MarketsScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/MarketsScenarios.kt @@ -3,7 +3,6 @@ package com.tangem.scenarios import com.tangem.common.BaseTestCase import com.tangem.common.extensions.SwipeDirection import com.tangem.common.extensions.clickWithAssertion -import com.tangem.common.extensions.swipeMarketsBlock import com.tangem.common.extensions.swipeVertical import com.tangem.screens.onMainScreen import com.tangem.screens.onMarketsExchangesScreen @@ -55,7 +54,7 @@ fun BaseTestCase.openMarketsScreen() { synchronizeAddresses() } step("Open 'Markets' screen") { - swipeMarketsBlock(SwipeDirection.UP) + onMainScreen { searchThroughMarketPlaceholder.performClick() } waitForIdle() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt index 45000fa4ad..b8a7826147 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt @@ -73,7 +73,7 @@ class TotalBalanceUpdateTest : BaseTestCase() { onMainScreen { totalBalanceText.assertTextContains(TOTAL_BALANCE) } } step("Open 'Markets screen'") { - swipeMarketsBlock(SwipeDirection.UP) + onMainScreen { searchThroughMarketPlaceholder.performClick() } waitForIdle() } step("Click on $tokenTitle token") { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/markets/MarketsExchangesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/markets/MarketsExchangesTest.kt index fd470cb89d..fc17e54653 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/markets/MarketsExchangesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/markets/MarketsExchangesTest.kt @@ -12,6 +12,7 @@ import com.tangem.scenarios.assertMarketsExchangesScreen import com.tangem.scenarios.openMainScreen import com.tangem.scenarios.openMarketsExchangesScreen import com.tangem.scenarios.synchronizeAddresses +import com.tangem.screens.onMainScreen import com.tangem.screens.onMarketsExchangesScreen import com.tangem.screens.onMarketsScreen import dagger.hilt.android.testing.HiltAndroidTest @@ -52,7 +53,7 @@ class MarketsExchangesTest : BaseTestCase() { synchronizeAddresses() } step("Open 'Markets' screen") { - swipeMarketsBlock(SwipeDirection.UP) + onMainScreen { searchThroughMarketPlaceholder.performClick() } waitForIdle() } step("Click on '$tokenName' token") { diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt index c7c56864ec..06314f32d7 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt @@ -25,9 +25,8 @@ class AnalyticsChain( val interceptor = CardContextInterceptor(previousChainResult) val params = event.params.toMutableMap() interceptor.intercept(params) - event.params = params.toMap() - Analytics.send(event) + Analytics.send(event.withParams(params.toMap())) return previousChainResult.right() } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index c4a6e936fc..797c3ce6da 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -238,9 +238,8 @@ internal class DefaultTangemSdkManager( val interceptor = CardContextInterceptor(scanResponse) val params = analyticsEvent.params.toMutableMap() interceptor.intercept(params) - analyticsEvent.params = params.toMap() - Analytics.send(event = analyticsEvent) + Analytics.send(event = analyticsEvent.withParams(params.toMap())) } .doOnFailure { tangemError -> (tangemError as? TangemSdkError)?.let { error -> diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index fe9115cfec..893ca50431 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -170,6 +170,14 @@ sealed class NotificationUM(val config: NotificationConfig) { subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text), ) + data class NetworkAccountNotFunded(val coinName: String) : Error( + title = resourceReference(R.string.alert_failed_to_send_transaction_title), + subtitle = resourceReference( + id = R.string.no_account_generic, + formatArgs = wrappedList(coinName), + ), + ) + data object DestinationTagRequired : Error( title = resourceReference(id = R.string.send_validation_destination_tag_required_title), subtitle = resourceReference(id = R.string.send_validation_destination_tag_required_description), diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 4e0e7d90e5..b52325095e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -136,14 +136,24 @@ object NotificationsFactory { // No need to show reserve amount warning if fee currency is unknown for token transfer return } else if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingCoinAmount) { - add( - NotificationUM.Error.ReserveAmount( - reserveAmount.format { - crypto(feeCryptoCurrency ?: cryptoCurrency) - }, - ), - ) + // account not funded, sending coin amount < reserve (send coin with less amount OR send any token) + + if (cryptoCurrency is CryptoCurrency.Coin) { + // Try to send coin amount less than reserve amount (e.g. less than 1 XLM in Stellar) + add( + NotificationUM.Error.ReserveAmount( + reserveAmount.format { + crypto(feeCryptoCurrency ?: cryptoCurrency) + }, + ), + ) + } else { + checkNotNull(feeCryptoCurrency) + // Try to send any token (e.g. USDC in Stellar, but account not funded -> user must send XLM at first) + add(NotificationUM.Error.NetworkAccountNotFunded(coinName = feeCryptoCurrency.name)) + } } + // TODO: check the RECEIVER account trustline before sending } fun MutableList.addMinimumAmountErrorNotification( diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt index 8c12f046df..d259b2ba00 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt @@ -6,8 +6,31 @@ package com.tangem.core.analytics.models open class AnalyticsEvent( val category: String, val event: String, - var params: Map = mapOf(), + val params: Map = mapOf(), ) { - val id: String = "[$category] $event" + + fun withParams(newParams: Map) = AnalyticsEvent(category, event, newParams) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AnalyticsEvent + + if (category != other.category) return false + if (event != other.event) return false + if (params != other.params) return false + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + var result = category.hashCode() + result = 31 * result + event.hashCode() + result = 31 * result + params.hashCode() + result = 31 * result + id.hashCode() + return result + } } \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index d6571cb78c..5b93790744 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -91,16 +91,16 @@ object Analytics : GlobalAnalyticsEventHandler { if (event is OneTimePerSessionEvent && !shouldSendThrottledEvent(event)) { return@launch } - event.params = applyParamsInterceptors(event) - val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) } + val eventWithParams = event.withParams(applyParamsInterceptors(event)) + val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(eventWithParams) } analyticsMutex.withLock { when { - eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(event) } - eventFilter.canBeSent(event) -> { + eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(eventWithParams) } + eventFilter.canBeSent(eventWithParams) -> { analyticsHandlers - .filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) } - .forEach { handler -> handler.send(event) } + .filter { handler -> eventFilter.canBeConsumedByHandler(handler, eventWithParams) } + .forEach { handler -> handler.send(eventWithParams) } } } } @@ -109,10 +109,10 @@ object Analytics : GlobalAnalyticsEventHandler { override fun sendErrorEvent(event: AnalyticsEvent) { analyticsScope.launch { - event.params = applyParamsInterceptors(event) + val eventWithParams = event.withParams(applyParamsInterceptors(event)) analyticsMutex.withLock { analyticsHandlers.filterIsInstance() - .forEach { handler -> handler.sendErrorEvent(event) } + .forEach { handler -> handler.sendErrorEvent(eventWithParams) } } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index 7629ed6986..af950e8bfe 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -242,6 +242,33 @@ fun BigDecimalFiatFormat.anyDecimals(decimals: Int): BigDecimalFormat = BigDecim .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) } +/** + * Formats fiat amount following the pattern: + * + * 123 -> $123 + * + * 123.1 -> $123.10 + * + * 123.10 -> $123.10 + * + * 123.456 -> $123.46 + */ +fun BigDecimalFiatFormat.optionalDecimals(): BigDecimalFormat = BigDecimalFormat { value -> + val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) + + val hasFraction = value.stripTrailingZeros().scale() > 0 + val defaultDigits = formatterCurrency.defaultFractionDigits + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + minimumFractionDigits = if (hasFraction) defaultDigits else 0 + maximumFractionDigits = defaultDigits + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(value) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) +} + // == Helpers == private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt index fcc2eca898..a5133d16d0 100644 --- a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt @@ -2,6 +2,9 @@ package com.tangem.core.ui.format.bigdecimal import com.google.common.truth.Truth import org.junit.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource import java.math.BigDecimal import java.util.Locale @@ -342,4 +345,37 @@ internal class BigDecimalFiatFormatTest { Truth.assertThat(formatted) .isEqualTo("-" + "0.50".addUsdSymbolLeft()) } + + @ParameterizedTest + @MethodSource("provideTestCasesForOptionalDecimals") + fun `GIVEN amount WHEN format with optionalDecimals THEN correct answer`( + amount: String, + answer: String, + ) { + val testValue = BigDecimal(amount) + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).optionalDecimals() + } + + Truth.assertThat(formatted).isEqualTo(answer) + } + + private companion object { + @JvmStatic + fun provideTestCasesForOptionalDecimals() = listOf( + Arguments.of("123", "$123"), + Arguments.of("123.1", "$123.10"), + Arguments.of("123.10", "$123.10"), + Arguments.of("123.456", "$123.46"), + Arguments.of("0", "$0"), + Arguments.of("0.1", "$0.10"), + Arguments.of("0.12", "$0.12"), + Arguments.of("0.127", "$0.13"), + ) + } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt index a8229e803a..5b3bc6f428 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt @@ -194,20 +194,36 @@ sealed class TangemPayAnalyticsEvents( ) class ReplaceCardClicked : TangemPayAnalyticsEvents( - categoryName = "Visa Screen", + categoryName = "Visa Card Management", event = "Visa Replace Card Clicked", ) class ReplaceCardConfirmationPopupOpened : TangemPayAnalyticsEvents( - categoryName = "Visa Screen", + categoryName = "Visa Card Management", event = "Visa Replace Card Confirmation Popup Opened", ) class ReplaceCardConfirmed : TangemPayAnalyticsEvents( - categoryName = "Visa Screen", + categoryName = "Visa Card Management", event = "Visa Replace Card Confirmed", ) + class LimitChangeClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Card Management", + event = "Visa Daily Limit Change Clicked", + ) + + class LimitManagementOpened : TangemPayAnalyticsEvents( + categoryName = "Visa Card Management", + event = "Visa Limit Management Screen Opened", + ) + + data class LimitChangeConfirmed(val amount: String) : TangemPayAnalyticsEvents( + categoryName = "Visa Card Management", + event = "Visa Set Limits Confirmed", + params = mapOf("amount" to amount), + ) + class MainVisaPermanentBannerClicked : TangemPayAnalyticsEvents( categoryName = "Visa Onboarding", event = "Visa Permanent Banner Clicked", diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 9fc28e116c..09b5c11731 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1223,6 +1223,23 @@ internal class SwapInteractorImpl @Inject constructor( }, ) + val fee = when (txFeeSealedState) { + is TxFeeSealedState.Component -> txFeeSealedState.txFee.fee.amount.value + is TxFeeSealedState.Legacy -> { + when (val txFee = txFeeSealedState.txFeeState) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.MultipleFeeState -> txFee.priorityFee.fee.amount.value + is TxFeeState.SingleFeeState -> txFee.fee.fee.amount.value + } + } + } + + val feeState = getFeeState( + fromSwapCurrencyStatus = fromSwapCurrencyStatus, + fee = fee, + spendAmount = amount, + ) + when (provider.type) { ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { val state = updatePermissionState( @@ -1236,26 +1253,11 @@ internal class SwapInteractorImpl @Inject constructor( state.copy( preparedSwapConfigState = state.preparedSwapConfigState.copy( isBalanceEnough = isBalanceWithoutFeeEnough, + feeState = feeState, ), ) } ExchangeProviderType.CEX -> { - val fee = when (txFeeSealedState) { - is TxFeeSealedState.Component -> txFeeSealedState.txFee.fee.amount.value - is TxFeeSealedState.Legacy -> { - when (val txFee = txFeeSealedState.txFeeState) { - TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> txFee.priorityFee.fee.amount.value - is TxFeeState.SingleFeeState -> txFee.fee.fee.amount.value - } - } - } - - val feeState = getFeeState( - fromSwapCurrencyStatus = fromSwapCurrencyStatus, - fee = fee, - spendAmount = amount, - ) swapState.copy( permissionState = PermissionDataState.Empty, preparedSwapConfigState = PreparedSwapConfigState( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index dae7c0b988..cc39b1d373 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -302,23 +302,24 @@ internal class SwapNotificationsFactory( ) { if (hideFee) return val fromCurrency = quoteModel.fromTokenInfo.swapCurrencyStatus.currency - val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return - val shouldShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && + val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough + val shouldShowCoverWarning = !quoteModel.preparedSwapConfigState.isBalanceEnough && quoteModel.permissionState !is PermissionDataState.PermissionLoading && feeCryptoCurrencyStatus?.currency != fromCurrency - val isNotEnoughFee = + val isCEXProvider = quoteModel.swapProvider.type == ExchangeProviderType.CEX + + val isNotEnoughFee = feeEnoughState is SwapFeeState.NotEnough && !isCEXProvider || quoteModel.preparedSwapConfigState.includeFeeInAmount is IncludeFeeInAmount.BalanceNotEnough - val isGaslessAvailable = isGaslessFeeSupportedForNetwork(fromCurrency.network) && - quoteModel.swapProvider.type == ExchangeProviderType.CEX + val isGaslessAvailable = isGaslessFeeSupportedForNetwork(fromCurrency.network) && isCEXProvider if (shouldShowCoverWarning && !isGaslessAvailable || isNotEnoughFee) { add( SwapNotificationUM.Error.UnableToCoverFeeWarning( fromToken = fromCurrency, feeCurrency = feeCryptoCurrencyStatus?.currency, - currencyName = feeEnoughState.currencyName ?: fromCurrency.network.name, - currencySymbol = feeEnoughState.currencySymbol ?: fromCurrency.network.currencySymbol, + currencyName = feeEnoughState?.currencyName ?: fromCurrency.network.name, + currencySymbol = feeEnoughState?.currencySymbol ?: fromCurrency.network.currencySymbol, onConfirmClick = actions.onBuyClick, ), ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 65aad222b7..c4c9307346 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -382,8 +382,12 @@ private fun Content( onFocusChange = type.onFocusChanged, ) - LaunchedEffect(Unit) { - focusRequester.requestFocus() + LaunchedEffect(type.isEnabled) { + if (type.isEnabled) { + focusRequester.requestFocus() + } else { + focusRequester.freeFocus() + } } } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt index 3c5b2b8b2a..a3b81fab45 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.tangempay.limit.setup import androidx.compose.runtime.Stable +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -8,10 +9,10 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList -import com.tangem.core.ui.format.bigdecimal.anyDecimals import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode +import com.tangem.core.ui.format.bigdecimal.optionalDecimals import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.models.StatusSource import com.tangem.domain.models.account.PaymentAccountStatusValue @@ -20,6 +21,7 @@ import com.tangem.domain.models.account.requireCardWithId import com.tangem.domain.models.pay.TangemPayCardLimitPeriod import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase +import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute @@ -34,6 +36,7 @@ import java.math.BigDecimal import java.util.Currency import javax.inject.Inject +@Suppress("LongParameterList") @Stable @ModelScoped internal class TangemPayCardLimitSetupModel @Inject constructor( @@ -43,6 +46,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor( private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, private val setTangemPayCardLimitUseCase: SetTangemPayCardLimitUseCase, private val uiMessageSender: UiMessageSender, + private val analytics: AnalyticsEventHandler, ) : Model() { private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() @@ -70,6 +74,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor( ) init { + analytics.send(TangemPayAnalyticsEvents.LimitManagementOpened()) observeCardState() } @@ -134,6 +139,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor( val amount = uiState.value.amountFieldModel.value.toBigDecimalOrNull() ?: return modelScope.launch { uiState.update { it.copy(isSubmitButtonLoading = true) } + analytics.send(TangemPayAnalyticsEvents.LimitChangeConfirmed(amount.toPlainString())) setTangemPayCardLimitUseCase( cardId = cardId, userWalletId = userWalletId, @@ -168,7 +174,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor( id = R.string.tangempay_card_limit_setup_amount_subtitle, formatArgs = WrappedList( listOf( - MIN_LIMIT.format { fiat(currency.currencyCode, currency.symbol).anyDecimals(0) }, + MIN_LIMIT.format { fiat(currency.currencyCode, currency.symbol).optionalDecimals() }, ), ), ) @@ -177,8 +183,8 @@ internal class TangemPayCardLimitSetupModel @Inject constructor( id = R.string.tangempay_daily_limit_hint, formatArgs = WrappedList( listOf( - MIN_LIMIT.format { fiat(currency.currencyCode, currency.symbol).anyDecimals(0) }, - maxLimit.format { fiat(currency.currencyCode, currency.symbol).anyDecimals(0) }, + MIN_LIMIT.format { fiat(currency.currencyCode, currency.symbol).optionalDecimals() }, + maxLimit.format { fiat(currency.currencyCode, currency.symbol).optionalDecimals() }, ), ), ) @@ -191,7 +197,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor( BigDecimal("10000"), BigDecimal("25000"), ).map { preset -> - val label = preset.format { fiat(currency.currencyCode, currency.symbol).anyDecimals(0) } + val label = preset.format { fiat(currency.currencyCode, currency.symbol).optionalDecimals() } TangemPayCardLimitSetupUM.LimitPresetUM( label = label, onClick = { onPresetClick(preset) }, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt index 5c5b40b9ae..fc48eab649 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode +import com.tangem.core.ui.format.bigdecimal.optionalDecimals import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokenReceiveConfig @@ -104,9 +105,9 @@ internal class TangemPayCardPageModel @Inject constructor( TangemPayDailyLimitBlockState.Content( limit = limit.amount.format { val symbol = getJavaCurrencyByCode(status.currencyCode).symbol - fiat(status.currencyCode, symbol) + fiat(status.currencyCode, symbol).optionalDecimals() }, - onChangeClick = { router.push(TangemPayCardDetailsInnerRoute.LimitSetup) }, + onChangeClick = ::onClickLimitChange, ) } else { TangemPayDailyLimitBlockState.Error @@ -138,6 +139,11 @@ internal class TangemPayCardPageModel @Inject constructor( ) } + private fun onClickLimitChange() { + analytics.send(TangemPayAnalyticsEvents.LimitChangeClicked()) + router.push(TangemPayCardDetailsInnerRoute.LimitSetup) + } + private fun onClickChangePIN(isPinSet: Boolean) { if (!isPinSet) { router.push(TangemPayCardDetailsInnerRoute.ChangePIN) diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt index 41c46b2d00..13c12f9f43 100644 --- a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt @@ -1,6 +1,7 @@ package com.tangem.features.tangempay.limit.setup import com.google.common.truth.Truth.assertThat +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.MutableParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender @@ -15,20 +16,20 @@ import com.tangem.domain.models.pay.TangemPayCardLimitPeriod import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase +import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.flow.flowOf import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import java.math.BigDecimal -@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class TangemPayCardLimitSetupModelTest { private val cardId = "test_card_id" @@ -38,6 +39,7 @@ internal class TangemPayCardLimitSetupModelTest { private val uiMessageSender: UiMessageSender = mockk(relaxed = true) private val setLimitUseCase: SetTangemPayCardLimitUseCase = mockk(relaxed = true) private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk() + private val analytics: AnalyticsEventHandler = mockk(relaxed = true) private val initialCard = TangemPayCard( id = cardId, @@ -93,6 +95,7 @@ internal class TangemPayCardLimitSetupModelTest { paymentAccountStatusSupplier = paymentAccountStatusSupplier, setTangemPayCardLimitUseCase = setLimitUseCase, uiMessageSender = uiMessageSender, + analytics = analytics, ) } @@ -122,7 +125,29 @@ internal class TangemPayCardLimitSetupModelTest { } @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Analytics { + @Test + fun `GIVEN model WHEN init THEN LimitManagementOpened is sent`() { + val model = createModel() + + verify(exactly = 1) { analytics.send(TangemPayAnalyticsEvents.LimitManagementOpened()) } + + model.onDestroy() + } + + @Test + fun `GIVEN model WHEN onSubmitClick THEN LimitChangeConfirmed is sent`() { + val model = createModel() + + model.uiState.value.amountFieldModel.onValueChange("100") + model.uiState.value.onSubmitClick() + verify(exactly = 1) { analytics.send(TangemPayAnalyticsEvents.LimitChangeConfirmed("100")) } + + model.onDestroy() + } + } + + @Nested inner class Presets { @Test @@ -146,14 +171,17 @@ internal class TangemPayCardLimitSetupModelTest { } } - private fun provideTestCases() = listOf( - Arguments.of("0", false), - Arguments.of("0.99", false), - Arguments.of("1", true), - Arguments.of("100", true), - Arguments.of("-1", false), - Arguments.of("", false), - Arguments.of("abc", true), - Arguments.of("1001", false), - ) + private companion object { + @JvmStatic + fun provideTestCases() = listOf( + Arguments.of("0", false), + Arguments.of("0.99", false), + Arguments.of("1", true), + Arguments.of("100", true), + Arguments.of("-1", false), + Arguments.of("", false), + Arguments.of("abc", true), + Arguments.of("1001", false), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index c844afcbdf..d802da3a1a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -354,7 +354,7 @@ internal enum class Wallet2CobrandImage( WinterSakura( cards2ResId = R.drawable.ill_winter_sakura_card2_120_106, cards3ResId = R.drawable.ill_winter_sakura_card3_120_106, - batchIds = setOf("AF990053", "AF990054", "AF990055"), + batchIds = setOf("AF990053", "AF990054", "AF990055", "AF990074", "AF990075", "AF990076"), ), LockedMoney(