Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-02 12:17:12 +03:00
parent 17d7fb3c02
commit 2934b42af5
29 changed files with 360 additions and 49 deletions

8
.gitignore vendored
View file

@ -3,6 +3,14 @@
# Built application files
/build
/buildSrc
**/build/.transforms/**
**/build/classes/**
**/build/generated/**
**/build/intermediates/**
**/build/kotlin/**
**/build/libs/**
**/build/outputs/**
**/build/tmp/**
# Local configuration file (sdk path, etc)
local.properties

View file

@ -120,6 +120,12 @@ internal object OnrampDomainModule {
return OnrampSaveTransactionUseCase(onrampTransactionRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampSepaAvailableUseCase(onrampRepository: OnrampRepository): OnrampSepaAvailableUseCase {
return OnrampSepaAvailableUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampUpdateTransactionStatusUseCase(

View file

@ -207,6 +207,7 @@ internal class ChildFactory @Inject constructor(
userWalletId = route.userWalletId,
cryptoCurrency = route.currency,
source = route.source,
launchSepa = route.launchSepa,
),
componentFactory = onrampComponentFactory,
)

View file

@ -238,6 +238,7 @@ sealed class AppRoute(val path: String) : Route {
val source: OnrampSource,
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
val launchSepa: Boolean = false,
) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}

View file

@ -878,7 +878,7 @@
<string name="send_extras_hint_memo">Memo</string>
<string name="send_fee_unreachable_error_text">Compruebe su conexión de red</string>
<string name="send_fee_unreachable_error_title">Información de tarifa de red no accesible</string>
<string name="send_from_title">Tú envías</string>
<string name="send_from_title">Envía</string>
<string name="send_from_wallet_android">Desde</string>
<string name="send_from_wallet_name">De %s</string>
<string name="send_gas_limit">Límite de gas</string>
@ -951,10 +951,13 @@
<string name="send_with_swap_confirm_title">Intercambiar y enviar</string>
<string name="send_with_swap_convert_token_alert_message">¿Continuar con la conversión? Esto borrará sus datos anteriores.</string>
<string name="send_with_swap_convert_token_alert_title">Confirmar Conversión</string>
<string name="send_with_swap_correct_recipient_network_notification_message">El envío de cualquier otra moneda supondrá su pérdida irreversible.</string>
<string name="send_with_swap_correct_recipient_network_notification_title">Seleccione la red de destino correcta</string>
<string name="send_with_swap_notification_text">Envía cualquier token y lo convertiremos en el camino. Su destinatario obtiene exactamente lo que necesita, sin problemas.</string>
<string name="send_with_swap_recipient_amount_success_title">El destinatario recibirá</string>
<string name="send_with_swap_recipient_amount_text">Al destinatario</string>
<string name="send_with_swap_recipient_amount_title">Cantidad a recibir</string>
<string name="send_with_swap_recipient_get_amount">El destinatario recibe %s</string>
<string name="send_with_swap_remove_convert_alert_message">¿Seguro que desea cancelar la conversión? Se borrarán sus datos anteriores.</string>
<string name="send_with_swap_title">Enviar con swap</string>
<string name="sent_transaction_sent_title">Transacción enviada</string>

View file

@ -1043,13 +1043,13 @@
<string name="send_with_swap_change_token_alert_message">受信トークンを変更してもよろしいですか? 変更すると、以前入力したデータがリセットされます。</string>
<string name="send_with_swap_change_token_alert_title">トークンの変更</string>
<string name="send_with_swap_confirm_title">スワップして送信</string>
<string name="send_with_swap_convert_token_alert_message">変換を続行しますか? これにより以前のデータは消去されます。</string>
<string name="send_with_swap_convert_token_alert_message">スワップを実行しますか?これにより以前のデータは消去されます。</string>
<string name="send_with_swap_convert_token_alert_title">変換を確定</string>
<string name="send_with_swap_correct_recipient_network_notification_message">その他の通貨を送信すると、取り返しのつかない損失が発生します。</string>
<string name="send_with_swap_correct_recipient_network_notification_title">正しい受信者ネットワークを選択してください</string>
<string name="send_with_swap_notification_text">受け取るトークンを自由に選んでください。受信者は、あなたが選択したトークンをシームレスに受け取ります。</string>
<string name="send_with_swap_recipient_amount_success_title">受信者受け取ります</string>
<string name="send_with_swap_recipient_amount_text">取人</string>
<string name="send_with_swap_notification_text">受け取るトークンを選んでください。受信者は、あなたが選択したトークンをシームレスに受け取ります。</string>
<string name="send_with_swap_recipient_amount_success_title">受信者受け取ります</string>
<string name="send_with_swap_recipient_amount_text">信者</string>
<string name="send_with_swap_recipient_amount_title">受取金額</string>
<string name="send_with_swap_recipient_get_amount">受信者は%sを取得します</string>
<string name="send_with_swap_remove_convert_alert_message">変換をキャンセルしてもよろしいですか?以前のデータは消去されます。</string>

View file

@ -1,5 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="account_details_archive_action">Архив</string>
<string name="account_details_archive_description">Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно</string>
<string name="account_form_placeholder_edit_account">Аккаунт</string>
<string name="account_form_placeholder_new_account">Новый аккаунт</string>
<string name="account_unsaved_dialog_action_first">Продолжить редактирование</string>
<string name="account_unsaved_dialog_title">Несохраненные изменения</string>
<string name="action_buttons_buy_empty_search_message">Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для покупки.</string>
<string name="action_buttons_sell_empty_search_message">Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для продажи.</string>
<string name="action_buttons_sell_navigation_bar_title">Продать</string>
@ -309,6 +315,7 @@
<string name="domain_receive_assets_legacy_address">Legacy адрес</string>
<string name="domain_receive_assets_navigation_title">Получить активы</string>
<string name="domain_receive_assets_onboarding_description">Отправка средств в другой сети может повлечь потерю средств.</string>
<string name="domain_receive_assets_onboarding_network_name">%s сети</string>
<string name="domain_receive_assets_onboarding_title">Отправляйте средства, используя только</string>
<string name="email_preface_wc_error">Привет, команда поддержки, у меня возникла ошибка с кодом: %s</string>
<string name="email_subject_wc_error">Ошибка WalletConnect</string>
@ -410,7 +417,16 @@
<string name="home_button_scan">Сканировать</string>
<string name="hot_crypto_add_token_subtitle">в %s</string>
<string name="hot_crypto_token_network">В сети %s</string>
<string name="hw_access_code_create_alert_title">Вы уверены, что хотите прервать процесс создания кода доступа?</string>
<string name="hw_backup_hardware_title">Аппаратный кошелёк</string>
<string name="hw_create_seed_description">Будьте в курсе новых функций и новостей</string>
<string name="hw_upgrade_error_card_already_has_wallet">Это устройство не может быть использовано для апгрейда, оно уже содержит другой кошелек.</string>
<string name="hw_upgrade_error_wallet2_card_required">Во время операции произошла ошибка.</string>
<string name="hw_upgrade_key_migration_description">Приватные ключи будут перемещены из приложения в вашу Tangem карту или кольцо</string>
<string name="hw_upgrade_key_migration_title">Миграция ключей</string>
<string name="hw_upgrade_start_action">Начать апгрейд</string>
<string name="hw_upgrade_start_title">Tangem кошелек</string>
<string name="hw_upgrade_to_cold_banner_title">Сделать апгрейд кошелька до аппаратной версии.</string>
<string name="information_generated_with_ai">Эта информация была сгенерирована ИИ.\nНажмите здесь, если обнаружили ошибку.</string>
<string name="initial_message_change_access_code_body">Чтобы изменить код доступа, приложите карту или кольцо как показано выше и не убирайте до окончания операции</string>
<string name="initial_message_change_passcode_body">Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции</string>
@ -928,7 +944,7 @@
<string name="send_with_swap_change_token_alert_message">Вы уверены, что хотите изменить токен для получения? Это действие сбросит ранее введённые данные.</string>
<string name="send_with_swap_change_token_alert_title">Изменение токена</string>
<string name="send_with_swap_confirm_title">Обмен и отправка</string>
<string name="send_with_swap_convert_token_alert_message">Продолжить с конвертацией? Это действие удалит предыдущие данные</string>
<string name="send_with_swap_convert_token_alert_message">Продолжить с обменом? Это действие удалит предыдущие данные</string>
<string name="send_with_swap_convert_token_alert_title">Подтвердить конвертацию</string>
<string name="send_with_swap_notification_text">Выберите любой токен к получению. Ваш адресат получит ровно то, что вы выбрали — без лишних сложностей.</string>
<string name="send_with_swap_recipient_amount_success_title">Будет получено</string>
@ -1100,6 +1116,7 @@
<string name="swapping_fee_estimation_error_text">Ошибка расчета комиссии. Пожалуйста, отправьте информацию в поддержку.</string>
<string name="swapping_from_title">Вы отправляете</string>
<string name="swapping_high_price_impact_description">Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.</string>
<string name="swapping_high_price_impact_title">Высокое влияние на цену</string>
<string name="swapping_insufficient_funds">Недостаточно средств</string>
<string name="swapping_permission_header">Дать разрешение</string>
<string name="swapping_swap_action">Обменять</string>
@ -1228,9 +1245,11 @@
<string name="wallet_promo_banner_button_title">Получить с 10% скидкой</string>
<string name="wallet_promo_banner_description">Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменивайте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. </string>
<string name="wallet_promo_banner_title">Откройте Tangem Wallet</string>
<string name="wallet_settings_change_access_code_title">Изменить Код Доступа</string>
<string name="wallet_settings_push_notifications_description">Получайте уведомления о входящих транзакциях в кошельке и обновлениях Tangem.</string>
<string name="wallet_settings_push_notifications_huawei_warning">В настоящий момент push-уведомления могут не работать на устройствах Huawei. Мы уже работаем над решением этой проблемы и планируем исправление в ближайших обновлениях. Спасибо за понимание!</string>
<string name="wallet_settings_push_notifications_title">Уведомления о транзакциях</string>
<string name="wallet_settings_set_access_code_title">Установить Код Доступа</string>
<string name="wallet_settings_title">Настройки кошелька</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Используйте %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку</string>

View file

@ -1367,6 +1367,7 @@
<string name="wc_errors_proposal_expired_subtitle">Будь ласка, згенеруйте новий URI та спробуйте ще раз</string>
<string name="wc_errors_proposal_expired_title">Термін для з’єднання минув</string>
<string name="wc_estimated_wallet_changes">Прогнозовані зміни</string>
<string name="wc_fee_suggested">Пропоновано %s</string>
<string name="wc_insufficient_warning_subtitle">Поповніть баланс, щоб покрити комісію мережі</string>
<string name="wc_insufficient_warning_title">Недостатньо %1$s</string>
<string name="wc_missing_required_network_description">Додайте %s мережі до вашого портфелю для цього гаманця</string>

View file

@ -1060,7 +1060,7 @@
<string name="send_with_swap_change_token_alert_message">Are you sure you want to change the receiving token? This will reset your previously entered data.</string>
<string name="send_with_swap_change_token_alert_title">Changing token</string>
<string name="send_with_swap_confirm_title">Swap and send</string>
<string name="send_with_swap_convert_token_alert_message">Proceed with conversion? This will clear your previous data.</string>
<string name="send_with_swap_convert_token_alert_message">Proceed with swap? This will clear your previous data.</string>
<string name="send_with_swap_convert_token_alert_title">Confirm Conversion</string>
<string name="send_with_swap_correct_recipient_network_notification_message">Sending any other currency will result in its irreversible loss.</string>
<string name="send_with_swap_correct_recipient_network_notification_title">Select the correct recipient network</string>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1,6 +1,8 @@
package com.tangem.utils.coroutines
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@ -54,4 +56,25 @@ fun CoroutineScope.launchOnCancellation(block: suspend () -> Unit) {
}
}
}
}
@Suppress("LongParameterList", "MagicNumber")
inline fun <T1, T2, T3, T4, T5, T6, R> combine6(
flow1: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
flow4: Flow<T4>,
flow5: Flow<T5>,
flow6: Flow<T6>,
crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R,
): Flow<R> = combine(flow1, flow2, flow3, flow4, flow5, flow6) { arr ->
@Suppress("UNCHECKED_CAST")
transform(
arr[0] as T1,
arr[1] as T2,
arr[2] as T3,
arr[3] as T4,
arr[4] as T5,
arr[5] as T6,
)
}

View file

@ -261,6 +261,48 @@ internal class DefaultOnrampRepository(
storeOnrampPairs(pairs = onrampPairs.await(), providers = providers.await())
}
override suspend fun hasMercuryoSepaMethod(
userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
): Boolean {
return withContext(dispatchers.io) {
val onrampPairs =
safeApiCall(
call = {
onrampApi.getPairs(
userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode(
userWallet = userWallet,
appPreferencesStore = appPreferencesStore,
),
body = OnrampPairsRequest(
fromCurrencyCode = currency.code,
countryCode = country.code,
to = listOf(
OnrampDestinationDTO(
contractAddress = cryptoCurrency.getContractAddress(),
network = cryptoCurrency.network.backendId,
),
),
),
).bind()
},
onError = {
Timber.w(it, "Unable to fetch onramp pairs")
throw it
},
)
val mercuryoProvider = onrampPairs.map { it.providers }.flatten()
.find { it.providerId == MERCURYO_PROVIDER_ID }
val hasSepaMethod = mercuryoProvider?.paymentMethods?.any { it == SEPA_METHOD_ID } ?: false
hasSepaMethod
}
}
override suspend fun fetchQuotes(userWallet: UserWallet, cryptoCurrency: CryptoCurrency, amount: Amount) =
withContext(dispatchers.io) {
val pairs = requireNotNull(pairsStore.getSyncOrNull(PAIRS_KEY)) {
@ -554,5 +596,8 @@ internal class DefaultOnrampRepository(
const val PROVIDER_THEME_LIGHT = "light"
const val REDIRECT_URL = "https://tangem.com/onramp"
const val SEPA_METHOD_ID = "sepa"
const val MERCURYO_PROVIDER_ID = "mercuryo"
}
}

View file

@ -38,24 +38,19 @@ internal class DefaultPromoRepository(
key = PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name),
default = true,
).map { shouldShow ->
if (promoId == PromoId.Referral) {
runCatching {
when (promoId) {
PromoId.Referral -> runCatching {
!referralRepository.isReferralParticipant(userWalletId) && shouldShow
}.getOrDefault(false)
} else {
shouldShow
PromoId.Sepa -> shouldShow
}
}
}
override fun isReadyToShowTokenPromo(promoId: PromoId): Flow<Boolean> {
return if (promoId == PromoId.Referral) {
flowOf(false)
} else {
appPreferencesStore.get(
PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name),
default = false,
)
return when (promoId) {
PromoId.Referral -> flowOf(false)
PromoId.Sepa -> flowOf(false)
}
}

View file

@ -6,4 +6,5 @@ enum class OnrampSource(val analyticsName: String) {
TOKEN_LONG_TAP("Long Tap"),
TOKEN_DETAILS("Token"),
MARKETS("Markets"),
SEPA_BANNER("SEPA Banner"),
}

View file

@ -0,0 +1,80 @@
package com.tangem.domain.onramp
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
class OnrampSepaAvailableUseCase(
private val repository: OnrampRepository,
) {
suspend operator fun invoke(
userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
): Boolean {
if (country.code !in SEPA_AVAILABLE_COUNTRY_CODES) {
return false
}
return Either.catch {
repository.hasMercuryoSepaMethod(
userWallet = userWallet,
currency = currency,
country = country,
cryptoCurrency = cryptoCurrency,
)
}.getOrElse { false }
}
companion object {
val SEPA_AVAILABLE_COUNTRY_CODES = listOf(
"AL", // Albania
"AD", // Andorra
"AT", // Austria
"BE", // Belgium
"BG", // Bulgaria
"HR", // Croatia
"CY", // Cyprus
"CZ", // Czech Republic
"DK", // Denmark
"EE", // Estonia
"FI", // Finland
"FR", // France
"DE", // Germany
"GR", // Greece
"HU", // Hungary
"IS", // Iceland
"IE", // Ireland
"IT", // Italy
"LV", // Latvia
"LI", // Liechtenstein
"LT", // Lithuania
"LU", // Luxembourg
"MT", // Malta
"MD", // Moldova
"MC", // Monaco
"ME", // Montenegro
"NL", // Netherlands
"MK", // North Macedonia
"NO", // Norway
"PL", // Poland
"PT", // Portugal
"RO", // Romania
"SM", // San Marino
"RS", // Serbia
"SK", // Slovakia
"SI", // Slovenia
"ES", // Spain
"SE", // Sweden
"CH", // Switzerland
"GB", // United Kingdom
"VA", // Vatican City
)
}
}

View file

@ -15,6 +15,12 @@ interface OnrampRepository {
suspend fun getCountriesSync(): List<OnrampCountry>?
suspend fun getCountryByIp(userWallet: UserWallet): OnrampCountry
suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus
suspend fun hasMercuryoSepaMethod(
userWallet: UserWallet,
currency: OnrampCurrency,
country: OnrampCountry,
cryptoCurrency: CryptoCurrency,
): Boolean
suspend fun fetchCurrencies(userWallet: UserWallet)
suspend fun fetchCountries(userWallet: UserWallet): List<OnrampCountry>
suspend fun fetchPaymentMethodsIfAbsent(userWallet: UserWallet)

View file

@ -27,4 +27,5 @@ data class PromoBanner(
enum class PromoId {
Referral,
Sepa,
}

View file

@ -9,24 +9,24 @@ sealed class TokenSwapPromoAnalyticsEvent(
) : AnalyticsEvent(category = "Promotion", event = event, params = params) {
class NoticePromotionBanner(
source: AnalyticsParam.ScreensSources,
programName: ProgramName,
program: Program,
) : TokenSwapPromoAnalyticsEvent(
event = "Notice - Promotion Banner",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
"Program Name" to programName.name,
"Program Name" to program.programName,
),
)
class PromotionBannerClicked(
source: AnalyticsParam.ScreensSources,
programName: ProgramName,
program: Program,
action: BannerAction,
) : TokenSwapPromoAnalyticsEvent(
event = "Promo Banner Clicked",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
"Program Name" to programName.name,
"Program Name" to program.programName,
"Action" to action.action,
),
) {
@ -37,7 +37,8 @@ sealed class TokenSwapPromoAnalyticsEvent(
}
// Use it on new promo action
enum class ProgramName {
Empty,
enum class Program(val programName: String) {
Empty("Empty"),
Sepa("Sepa"),
}
}

View file

@ -8,7 +8,12 @@ import com.tangem.domain.models.wallet.UserWalletId
interface OnrampComponent : ComposableContentComponent {
data class Params(val userWalletId: UserWalletId, val cryptoCurrency: CryptoCurrency, val source: OnrampSource)
data class Params(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val source: OnrampSource,
val launchSepa: Boolean = false,
)
interface Factory : ComponentFactory<Params, OnrampComponent>
}

View file

@ -15,6 +15,7 @@ internal interface OnrampMainComponent : ComposableContentComponent {
val source: OnrampSource,
val openSettings: () -> Unit,
val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit,
val launchSepa: Boolean,
)
interface Factory : ComponentFactory<Params, OnrampMainComponent>

View file

@ -148,4 +148,8 @@ internal class OnrampStateFactory(
decimals = currency.precision,
type = AmountType.FiatType(currency.code),
)
companion object {
const val PREDEFINED_SEPA_AMOUNT = "100"
}
}

View file

@ -28,6 +28,7 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.main.OnrampMainComponent
import com.tangem.features.onramp.main.entity.*
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory.Companion.PREDEFINED_SEPA_AMOUNT
import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory
import com.tangem.features.onramp.providers.entity.SelectProviderResult
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
@ -64,6 +65,8 @@ internal class OnrampMainComponentModel @Inject constructor(
private val params: OnrampMainComponent.Params = paramsContainer.require()
private var isSepaLaunched = false
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
private val stateFactory = OnrampStateFactory(
@ -148,11 +151,16 @@ internal class OnrampMainComponentModel @Inject constructor(
if (country == null) return@onEach
_state.update {
if (it is OnrampMainComponentUM.InitialLoading) {
stateFactory.getReadyState(country.defaultCurrency)
val state = stateFactory.getReadyState(country.defaultCurrency)
if (params.launchSepa) {
onAmountValueChanged(PREDEFINED_SEPA_AMOUNT)
}
state
} else {
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
}
}
updatePairsAndQuotes()
},
)
@ -317,8 +325,18 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun selectOrUpdateQuote(quotes: List<OnrampQuote>): OnrampQuote? {
val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error }
val sepaQuote = if (params.launchSepa && !isSepaLaunched) {
isSepaLaunched = true
quotes.filterIsInstance<OnrampQuote.Data>().firstOrNull {
it.provider.id == MERCURYO_PROVIDER_ID && it.paymentMethod.id == SEPA_METHOD_ID
}
} else {
null
}
// Check if amount, country or currency has changed
val newQuote = if (checkLastInputState(quoteToCheck)) {
val newQuote = sepaQuote ?: if (checkLastInputState(quoteToCheck)) {
quoteToCheck
} else {
val state = state.value as? OnrampMainComponentUM.Content
@ -420,5 +438,8 @@ internal class OnrampMainComponentModel @Inject constructor(
private companion object {
const val UPDATE_DELAY = 10_000L
const val MERCURYO_PROVIDER_ID = "mercuryo"
const val SEPA_METHOD_ID = "sepa"
}
}

View file

@ -81,6 +81,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
),
)
},
launchSepa = params.launchSepa,
),
)
is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create(

View file

@ -38,7 +38,7 @@ internal class TokenDetailsNotificationsAnalyticsSender(
currency = cryptoCurrency,
)
is TokenDetailsNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action
program = TokenSwapPromoAnalyticsEvent.Program.Empty, // Use it on new promo action
source = AnalyticsParam.ScreensSources.Token,
)
is TokenDetailsNotification.KaspaIncompleteTransactionWarning -> TokenDetailsAnalyticsEvent.Notice.Reveal(

View file

@ -850,7 +850,7 @@ internal class TokenDetailsModel @Inject constructor(
analyticsEventsHandler.send(
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Token,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action
program = TokenSwapPromoAnalyticsEvent.Program.Empty, // Use it on new promo action
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
),
)
@ -863,7 +863,7 @@ internal class TokenDetailsModel @Inject constructor(
analyticsEventsHandler.send(
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Token,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action
program = TokenSwapPromoAnalyticsEvent.Program.Empty, // Use it on new promo action
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked,
),
)

View file

@ -19,6 +19,7 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
@ -75,7 +76,7 @@ internal interface WalletWarningsClickIntents {
fun onClosePromoClick(promoId: PromoId)
fun onPromoClick(promoId: PromoId)
fun onPromoClick(promoId: PromoId, cryptoCurrency: CryptoCurrency? = null)
fun onSupportClick()
@ -92,7 +93,7 @@ internal interface WalletWarningsClickIntents {
fun onFinishWalletActivationClick()
}
@Suppress("LongParameterList")
@Suppress("LargeClass", "LongParameterList")
@ModelScoped
internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val stateHolder: WalletStateController,
@ -260,26 +261,46 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onClosePromoClick(promoId: PromoId) {
analyticsEventHandler.send(
if (promoId == PromoId.Referral) {
MainScreen.ReferralPromoButtonDismiss
} else {
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
when (promoId) {
PromoId.Referral -> MainScreen.ReferralPromoButtonDismiss
PromoId.Sepa -> TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action
program = TokenSwapPromoAnalyticsEvent.Program.Sepa,
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
)
},
)
modelScope.launch(dispatchers.main) {
shouldShowPromoWalletUseCase.neverToShow(promoId)
}
}
override fun onPromoClick(promoId: PromoId) {
if (promoId == PromoId.Referral) {
val userWallet = getSelectedUserWallet() ?: return
analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate)
appRouter.push(AppRoute.ReferralProgram(userWalletId = userWallet.walletId))
override fun onPromoClick(promoId: PromoId, cryptoCurrency: CryptoCurrency?) {
val userWallet = getSelectedUserWallet() ?: return
when (promoId) {
PromoId.Referral -> {
analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate)
appRouter.push(AppRoute.ReferralProgram(userWalletId = userWallet.walletId))
}
PromoId.Sepa -> {
analyticsEventHandler.send(
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
program = TokenSwapPromoAnalyticsEvent.Program.Sepa,
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked,
),
)
cryptoCurrency ?: return
appRouter.push(
AppRoute.Onramp(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
source = OnrampSource.SEPA_BANNER,
launchSepa = true,
),
)
}
}
}

View file

@ -5,7 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent.ProgramName
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent.Program
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
@ -51,7 +51,11 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.NoteMigration -> MainScreen.NotePromo
is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
programName = ProgramName.Empty, // Use it on new promo action
program = Program.Empty, // Use it on new promo action
)
is WalletNotification.Sepa -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
program = Program.Sepa,
)
is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]

View file

@ -1,5 +1,8 @@
@file:Suppress("MaximumLineLength")
package com.tangem.feature.wallet.presentation.wallet.domain
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint
@ -14,9 +17,12 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.GetOnrampCountryUseCase
import com.tangem.domain.onramp.OnrampSepaAvailableUseCase
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
@ -24,11 +30,13 @@ import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.coroutines.combine6
import com.tangem.utils.extensions.isPositive
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -41,19 +49,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val backupValidator: BackupValidator,
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase,
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val onrampSepaAvailableUseCase: OnrampSepaAvailableUseCase,
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
) {
@Suppress("MagicNumber", "MaximumLineLength")
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver
return combine(
flow = tokenListStore.getOrThrow(userWallet.walletId),
return combine6(
flow1 = tokenListStore.getOrThrow(userWallet.walletId),
flow2 = isReadyToShowRateAppUseCase(),
flow3 = isNeedToBackupUseCase(userWallet.walletId),
flow4 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId),
flow5 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Referral),
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, seedPhraseIssueStatus, shouldShowReferralPromo ->
flow6 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa),
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, seedPhraseIssueStatus, shouldShowReferralPromo, shouldShowSepaBanner ->
buildList {
addUsedOutdatedDataNotification(maybeTokenList)
@ -63,6 +74,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo)
addSepaPromoNotification(userWallet, clickIntents, shouldShowSepaBanner)
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
addWarningNotifications(cardTypesResolver, maybeTokenList, isNeedToBackup, clickIntents)
@ -217,6 +230,39 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private suspend fun MutableList<WalletNotification>.addSepaPromoNotification(
userWallet: UserWallet,
clickIntents: WalletClickIntents,
shouldShowSepaPromo: Boolean,
) {
val currencies = getCryptoCurrenciesUseCase(userWalletId = userWallet.walletId).getOrElse {
Timber.e("Error on getting crypto currency list")
return
}
val bitcoinCurrency = currencies.find { isBitcoin(it.network.rawId) } ?: return
val country = getOnrampCountryUseCase.invokeSync(userWallet).getOrElse {
Timber.e("Error on getting onramp country")
return
}
val isSepaAvailable = onrampSepaAvailableUseCase(
userWallet = userWallet,
country = country,
currency = country.defaultCurrency,
cryptoCurrency = bitcoinCurrency,
)
addIf(
element = WalletNotification.Sepa(
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.Sepa) },
onClick = { clickIntents.onPromoClick(promoId = PromoId.Sepa, bitcoinCurrency) },
),
condition = shouldShowSepaPromo && isSepaAvailable,
)
}
private fun MutableList<WalletNotification>.addWarningNotifications(
cardTypesResolver: CardTypesResolver?,
tokenList: Lce<TokenListError, TokenList>,

View file

@ -287,4 +287,21 @@ sealed class WalletNotification(val config: NotificationConfig) {
iconSize = 54.dp,
),
)
data class Sepa(
val onCloseClick: () -> Unit,
val onClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.notification_sepa_title),
subtitle = resourceReference(R.string.notification_sepa_text),
iconResId = R.drawable.img_notification_sepa,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_sepa_button),
onClick = onClick,
),
iconSize = 54.dp,
),
)
}