Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-30 15:58:51 +03:00
commit acf110dbcc
30 changed files with 171 additions and 63 deletions

View file

@ -4,30 +4,38 @@ import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import java.util.WeakHashMap
import timber.log.Timber
import kotlin.reflect.KClass
class ForegroundActivityObserver {
object ForegroundActivityObserver {
private val activities = WeakHashMap<KClass<out Activity>, AppCompatActivity>()
private val activities = HashMap<KClass<out Activity>, AppCompatActivity>()
val foregroundActivity: AppCompatActivity?
get() = activities.entries
.firstOrNull { it.value?.isDestroyed == false }
.firstOrNull { entry ->
Timber.i("foregroundActivity: ${entry.key} | ${entry.value.isDestroyed}")
entry.value.isDestroyed == false
}
?.value
internal val callbacks: ActivityLifecycleCallbacks
get() = Callbacks()
internal inner class Callbacks : ActivityLifecycleCallbacks {
internal class Callbacks : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
override fun onActivityResumed(activity: Activity) {
activities[activity::class] = activity as? AppCompatActivity
Timber.i("onActivityResumed ${activity::class}")
if (activity is AppCompatActivity) {
Timber.i("onActivityResumed store activity")
activities[activity::class] = activity
}
}
override fun onActivityDestroyed(activity: Activity) {
Timber.i("onActivityDestroyed")
activities.remove(activity::class)
}

View file

@ -191,6 +191,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
override fun onCreate(savedInstanceState: Bundle?) {
Timber.i("onCreate")
// We need to call it before onCreate to prevent unnecessary activity recreation
installAppTheme()
@ -328,15 +329,18 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
override fun onStart() {
super.onStart()
Timber.i("onStart")
dialogManager.onStart(this)
}
override fun onStop() {
dialogManager.onStop()
super.onStop()
Timber.i("onStop")
}
override fun onDestroy() {
Timber.i("onDestroy")
// workaround: kill process when activity destroy to avoid state when lock() wallets
// and navigation to unlock screen was skipped because system kills activity but not process
if (BuildConfig.BUILD_TYPE != MOCKED_BUILD_TYPE) {

View file

@ -85,7 +85,7 @@ import timber.log.Timber
lateinit var store: Store<AppState>
lateinit var foregroundActivityObserver: ForegroundActivityObserver
val foregroundActivityObserver = ForegroundActivityObserver
internal lateinit var derivationsFinder: DerivationsFinder
open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider {
@ -246,6 +246,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
override fun onCreate() {
enableStrictModeInDebug()
preInit()
super.onCreate()
init()
}
@ -286,22 +287,25 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
// }
}
/**
* Initialize components that need to be initialized before [super.onCreate] is called
*/
private fun preInit() {
tangemAppLoggerInitializer.initialize()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
}
fun init() {
apiConfigsManager.initialize()
store = createReduxStore()
tangemAppLoggerInitializer.initialize()
Timber.i("APP STARTED")
if (BuildConfig.TESTER_MENU_ENABLED) {
Timber.i(featureTogglesManager.toString())
Timber.i(excludedBlockchainsManager.toString())
}
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
runBlocking {
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
}

View file

@ -23,10 +23,12 @@ class AppsFlyerAnalyticsHandler(
}
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
!data.isDebug -> AppsFlyerClient(data.application, data.config.appsFlyerApiKey, data.config.appsAppId)
data.isDebug && data.logConfig.appsflyer -> AppsFlyerLogClient(data.jsonConverter)
else -> null
}?.let { AppsFlyerAnalyticsHandler(it) }
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = null
// disabled for now until analytics strategy is defined
// when {
// !data.isDebug -> AppsFlyerClient(data.application, data.config.appsFlyerApiKey, data.config.appsAppId)
// data.isDebug && data.logConfig.appsflyer -> AppsFlyerLogClient(data.jsonConverter)
// else -> null
// }?.let { AppsFlyerAnalyticsHandler(it) }
}
}

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -19,7 +19,10 @@
<string name="account_archived_recover">回復する</string>
<string name="account_archived_recover_dialog_description">「 %1$s 」を回復しようとしています。</string>
<string name="account_archived_recover_dialog_title">アカウントを回復する</string>
<string name="account_archived_recover_error_message">すでにアクティブアカウント数の上限20件を超えています。復元するには1件アーカイブしてください。</string>
<string name="account_archived_recover_error_title">アカウントを復元できません</string>
<string name="account_archived_title">アーカイブ済み</string>
<string name="account_could_not_create">アカウントを作成できませんでした。しばらくしてからもう一度お試しください。</string>
<string name="account_create_success_message">アカウントを作成しました</string>
<string name="account_details_archive">アカウントをアーカイブする</string>
<string name="account_details_archive_action">アーカイブ</string>
@ -258,6 +261,7 @@
<string name="common_left">残り%1$s</string>
<string name="common_legacy_bitcoin_address">レガシービットコイン</string>
<string name="common_locked">ロックされています</string>
<string name="common_locked_wallets">ロックされたウォレット</string>
<string name="common_main_network">メインネットワーク</string>
<string name="common_month"></string>
<string name="common_network_fee_title">ネットワーク手数料</string>
@ -307,6 +311,7 @@
<string name="common_sign">署名</string>
<string name="common_sign_and_send">署名して送信</string>
<string name="common_skip">スキップ</string>
<string name="common_something_went_wrong">問題が発生しました</string>
<string name="common_stake">ステーキング</string>
<string name="common_staking">ステーキング</string>
<string name="common_start">始める</string>
@ -1143,7 +1148,7 @@
<string name="staking_notification_restake_rewards_text">獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。</string>
<string name="staking_notification_restake_text">再ステーキングを使うと、ステーキングを解除することなく、あるバリデータから別のバリデータに資金を移動できます。</string>
<string name="staking_notification_stake_entire_balance_text">残高のすべてをステーキングしようとしています。ステーキング解除や報酬請求にかかるネットワーク手数料をカバーするために、少額を残しておくことをお勧めします。</string>
<string name="staking_notification_ton_activate_account">TONでステーキングを開始するには、まず任意の金額の送金を行ってください。これにより、ウォレットがアクティブになります。</string>
<string name="staking_notification_ton_activate_account">TONのステーキングを開始するには、まず自分のアドレスに少額の取引を送信します。これによりウォレットが有効になります。</string>
<string name="staking_notification_ton_extra_reserve_info">取引を完了するには、ネットワーク手数料に加えて最大0.2TONが必要になる場合があります。未使用分は返金されます。</string>
<string name="staking_notification_ton_extra_reserve_is_required">この操作を続行するには、ネットワーク手数料に加えて0.2 TONが必要です。残高を補充してください。</string>
<string name="staking_notification_ton_extra_reserve_title">TONの準備金が必要です</string>
@ -1214,6 +1219,7 @@
<string name="story_web3_title">Web3.0対応</string>
<string name="sui_not_enough_coin_for_fee_description">続行するには少なくとも%1$sの受信取引が必要です</string>
<string name="sui_not_enough_coin_for_fee_title">残高不足</string>
<string name="swap_approve_description">承認すると、このスマートコントラクトが今後の取引でトークンを利用できるようになります。</string>
<string name="swap_fixed_rate">固定レート</string>
<string name="swap_give_permission_fee_footer">ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。</string>
<string name="swap_promo_text">より多くのトークンをより良いレートで、ウォレット内にて直接交換します。</string>
@ -1653,7 +1659,7 @@
<string name="yield_module_approve_sheet_title">承認を確定する</string>
<string name="yield_module_balance_info_sheet_subtitle">資産残高に関するテキスト [プレースホルダー]</string>
<string name="yield_module_balance_info_sheet_title">あなたの%sはAaveに預けられています</string>
<string name="yield_module_earn_badge">%s%を獲得</string>
<string name="yield_module_earn_badge">%1$s%% を獲得</string>
<string name="yield_module_earn_sheet_available_title">利用可能</string>
<string name="yield_module_earn_sheet_current_apy_title">現在のAPY</string>
<string name="yield_module_earn_sheet_my_funds_title">私の資金</string>
@ -1670,6 +1676,8 @@
<string name="yield_module_fee_policy_sheet_title">手数料ポリシー</string>
<string name="yield_module_main_view_approve_notification_description">ここに説明を入力してください。1〜3行が理想的です。[プレースホルダー]</string>
<string name="yield_module_main_view_approve_notification_title">トークン承認が必要</string>
<string name="yield_module_network_fee_unreachable_notification_description">ネットワーク接続を確認してください</string>
<string name="yield_module_network_fee_unreachable_notification_title">ネットワーク手数料についての情報にアクセスできません</string>
<string name="yield_module_promo_screen_auto_balance_subtitle">アカウントへの入金はすべて自動的にAaveに貸し出されます。</string>
<string name="yield_module_promo_screen_auto_balance_title">残高は自動的に計算されます</string>
<string name="yield_module_promo_screen_cash_out_subtitle">いつでも、即座に資金を送信、交換、売却できます。</string>

View file

@ -1298,6 +1298,8 @@
<string name="wallet_connect_error_wrong_card_selected">Неверная карта или кольцо выбрана в приложении Tangem</string>
<string name="wallet_connect_failed_to_build_tx">Не удалось создать транзакцию из данных Dapp. Код: %s</string>
<string name="wallet_connect_generic_error_with_code">Произошла непредвиденная ошибка. Код ошибки: %d. Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать — обратитесь в службу поддержки.</string>
<string name="wallet_connect_multiple_transactions">Транзакция в несколько шагов</string>
<string name="wallet_connect_multiple_transactions_description">Чтобы успешно обработать запрос, ваша транзакция будет разделена на несколько частей. Для завершения потребуется несколько приложений карты.</string>
<string name="wallet_connect_no_sessions_message">Нет открытых сессий WalletConnect</string>
<string name="wallet_connect_no_sessions_title">Упс. Нет сессий.</string>
<string name="wallet_connect_pairing_error">Не удалось создать пару WalletConnect: %1$s</string>
@ -1309,6 +1311,8 @@
<string name="wallet_connect_scanner_error_not_valid_card">Эту карту нельзя использовать с WalletConnect.</string>
<string name="wallet_connect_scanner_error_unsupported_network">Сеть не поддерживается. Пожалуйста, выберите другую сеть.</string>
<string name="wallet_connect_select_network">Выберите сеть</string>
<string name="wallet_connect_sending_multiple_explanation">Транзакция в процессе отправки. Пожалуйста, приложите карту несколько раз для её завершения.</string>
<string name="wallet_connect_sending_multiple_tx">Транзакция в обработке</string>
<string name="wallet_connect_sessions_title">Сессии WalletConnect</string>
<string name="wallet_connect_subtitle">Подключение к dApps</string>
<string name="wallet_connect_title">WalletConnect</string>

View file

@ -19,7 +19,10 @@
<string name="account_archived_recover">Recover</string>
<string name="account_archived_recover_dialog_description">Youre about to recover “%1$s”.</string>
<string name="account_archived_recover_dialog_title">Recover account</string>
<string name="account_archived_recover_error_message">You have already exceeded the limit of 20 active accounts. Archive one to recover</string>
<string name="account_archived_recover_error_title">Can\'t recover account</string>
<string name="account_archived_title">Archived</string>
<string name="account_could_not_create">We couldnt create account. Please try again later.</string>
<string name="account_create_success_message">Account created</string>
<string name="account_details_archive">Archive account</string>
<string name="account_details_archive_action">Archive</string>
@ -314,6 +317,7 @@
<string name="common_sign">Sign</string>
<string name="common_sign_and_send">Sign and send</string>
<string name="common_skip">Skip</string>
<string name="common_something_went_wrong">Something went wrong</string>
<string name="common_stake">Stake</string>
<string name="common_staking">Staking</string>
<string name="common_start">Start</string>
@ -927,6 +931,7 @@
<string name="receive_bottom_sheet_warning_message_full">Send only %s to this address. Sending any other currency will result in its irreversible loss.</string>
<string name="receive_bottom_sheet_warning_title">Send only %1$s on the %2$s network</string>
<string name="receive_token_description">Transfer funds from any wallet or exchange</string>
<string name="referral_address_for_rewards">Address for rewards</string>
<string name="referral_button_participate">Participate</string>
<string name="referral_error_failed_to_load_info">Failed to load the information about the referral program. Please try again later.</string>
<string name="referral_error_failed_to_load_info_with_reason">Failed to load the information about the referral program. Error code: %s. Please try again later.</string>
@ -1237,6 +1242,7 @@
<string name="story_web3_title">Web 3.0 Compatible</string>
<string name="sui_not_enough_coin_for_fee_description">An incoming transaction of at least %1$s is required to proceed</string>
<string name="sui_not_enough_coin_for_fee_title">Insufficient funds</string>
<string name="swap_approve_description">By approving, you allow the smart contract to use your tokens in future transactions.</string>
<string name="swap_fixed_rate">Fixed Rate</string>
<string name="swap_give_permission_fee_footer">The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.</string>
<string name="swap_promo_text">Exchange more tokens at better rates directly in your wallet.</string>
@ -1455,8 +1461,8 @@
<string name="wallet_connect_error_wrong_card_selected">Wrong card or ring selected in Tangem App</string>
<string name="wallet_connect_failed_to_build_tx">Failed to create transaction from Dapp data. Code: %s</string>
<string name="wallet_connect_generic_error_with_code">We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support</string>
<string name="wallet_connect_multiple_transactions">Multiple transactions</string>
<string name="wallet_connect_multiple_transactions_description">Youll need to tap your Tangem device a few times to complete this process.</string>
<string name="wallet_connect_multiple_transactions">Multi-Part Transaction</string>
<string name="wallet_connect_multiple_transactions_description">To process successfully, your transaction will be split into multiple parts. You\'ll need to tap your card several times to complete it.</string>
<string name="wallet_connect_no_sessions_message">No opened WalletConnect sessions</string>
<string name="wallet_connect_no_sessions_title">Ooops. No Sessions.</string>
<string name="wallet_connect_pairing_error">Failed to pairing WalletConnect session: %1$s</string>
@ -1468,8 +1474,8 @@
<string name="wallet_connect_scanner_error_not_valid_card">This card can\'t be used to establish WalletConnect session</string>
<string name="wallet_connect_scanner_error_unsupported_network">This network is not supported. Please select another network.</string>
<string name="wallet_connect_select_network">Select network</string>
<string name="wallet_connect_sending_multiple_explanation">We\'re processing the transaction</string>
<string name="wallet_connect_sending_multiple_tx">Sending your funds...</string>
<string name="wallet_connect_sending_multiple_explanation">The transaction is being processed. Please tap your card multiple times to complete it.</string>
<string name="wallet_connect_sending_multiple_tx">Transaction in progress</string>
<string name="wallet_connect_sessions_title">WalletConnect Sessions</string>
<string name="wallet_connect_subtitle">Connect to dApps</string>
<string name="wallet_connect_title">WalletConnect</string>
@ -1725,7 +1731,7 @@
<string name="yield_module_approve_sheet_title">Confirm approval</string>
<string name="yield_module_balance_info_sheet_subtitle">Text about your money in balance [PLACEHOLDER]</string>
<string name="yield_module_balance_info_sheet_title">Your %s is deposited in Aave</string>
<string name="yield_module_earn_badge">Earn %s%</string>
<string name="yield_module_earn_badge">Earn %1$s%%</string>
<string name="yield_module_earn_sheet_available_title">Available</string>
<string name="yield_module_earn_sheet_current_apy_title">Current APY</string>
<string name="yield_module_earn_sheet_my_funds_title">My Funds</string>
@ -1742,6 +1748,8 @@
<string name="yield_module_fee_policy_sheet_title">Fee policy</string>
<string name="yield_module_main_view_approve_notification_description">Write description here. In one, two or three lines will be awesome. [PLACEHOLDER]</string>
<string name="yield_module_main_view_approve_notification_title">Some token approve needed</string>
<string name="yield_module_network_fee_unreachable_notification_description">Check your network connection</string>
<string name="yield_module_network_fee_unreachable_notification_title">Network fee info unreachable</string>
<string name="yield_module_promo_screen_auto_balance_subtitle">Every top-up of your account will be lended to Aave automatically.</string>
<string name="yield_module_promo_screen_auto_balance_title">Your balance works automatically</string>
<string name="yield_module_promo_screen_cash_out_subtitle">Send, swap, or sell your funds instantly, anytime you want.</string>

View file

@ -14,6 +14,7 @@ import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.usecase.PrepareAndSignUseCase
import com.tangem.domain.transaction.usecase.SendLargeSolanaTransactionUseCase
import com.tangem.domain.walletconnect.WcAnalyticEvents.SolanaLargeTransactionStatus
import com.tangem.domain.walletconnect.error.parseSendError
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck
@ -59,10 +60,12 @@ internal class WcSolanaSignTransactionUseCase @AssistedInject constructor(
sendLargeSolanaTransactionUseCase(context.session.wallet as UserWallet.Cold, context.network, formattedHash)
.fold(
ifLeft = {
analytics.send(SolanaLargeTransactionStatus(SolanaLargeTransactionStatus.Status.Failed))
Timber.e(it.toString())
emit(state.toResult(parseSendError(it).left()))
},
ifRight = {
analytics.send(SolanaLargeTransactionStatus(SolanaLargeTransactionStatus.Status.Success))
val emptyRespond = ByteArray(0).formatAsSolanaSignature()
val respondResult = respondService.respond(rawSdkRequest, emptyRespond)
emit(state.toResult(respondResult))

View file

@ -110,6 +110,10 @@ sealed interface StakingIntegrationID {
val integrationId = blockchain.integrationId ?: return null
if (integrationId is Coin && currencyId.contractAddress != null) {
return null
}
return when (integrationId) {
is Coin -> integrationId
is EthereumToken -> {

View file

@ -139,9 +139,13 @@ class StakingIntegrationIDTest {
expected = StakingIntegrationID.Coin.Cardano,
),
CreateModel(
currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-token⚓"),
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩polygon-ecosystem-token⚓1234567890"),
expected = StakingIntegrationID.EthereumToken.Polygon,
),
CreateModel(
currencyId = CryptoCurrency.ID.fromValue(value = "token⟨SOLANA⟩solana⚓1234567890"),
expected = null,
),
)
}

View file

@ -236,6 +236,29 @@ sealed class WcAnalyticEvents(
enum class Source { Domain, SmartContract }
}
data class SolanaLargeTransaction(
val dappName: String,
) : WcAnalyticEvents(
event = "Solana Large Transaction",
params = mapOf(
AnalyticsParam.Key.DAPP_NAME to dappName,
),
)
data class SolanaLargeTransactionStatus(
val status: Status,
) : WcAnalyticEvents(
event = "Solana Large Transaction Status",
params = mapOf(
AnalyticsParam.Key.STATUS to status.value,
),
) {
enum class Status(val value: String) {
Success("Success"),
Failed("Failed"),
}
}
enum class DAppVerificationStatus(val status: String) {
Verified("Verified"),
Risky("Risky"),

View file

@ -12,6 +12,7 @@ internal interface ConfirmResidencyComponent : ComposableBottomSheetComponent {
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
val country: OnrampCountry,
val launchSepa: Boolean,
val onDismiss: () -> Unit,
)

View file

@ -9,12 +9,14 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.onramp.OnrampSaveDefaultCountryUseCase
import com.tangem.domain.onramp.OnrampSaveDefaultCurrencyUseCase
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent
import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig
import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.utils.model.EUR_CURRENCY
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@ -26,6 +28,7 @@ internal class ConfirmResidencyModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val router: Router,
private val saveDefaultCountryUseCase: OnrampSaveDefaultCountryUseCase,
private val onrampSaveDefaultCurrencyUseCase: OnrampSaveDefaultCurrencyUseCase,
paramsContainer: ParamsContainer,
) : Model() {
@ -54,6 +57,10 @@ internal class ConfirmResidencyModel @Inject constructor(
analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceConfirm(country.name))
modelScope.launch {
saveDefaultCountryUseCase.invoke(country)
if (params.launchSepa) {
onrampSaveDefaultCurrencyUseCase.invoke(EUR_CURRENCY)
}
params.onDismiss()
}
},

View file

@ -57,6 +57,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
userWalletId = params.userWalletId,
cryptoCurrency = params.cryptoCurrency,
country = config.country,
launchSepa = params.launchSepa,
onDismiss = {
model.bottomSheetNavigation.dismiss()
model.handleOnrampAvailable()

View file

@ -3,7 +3,7 @@ package com.tangem.features.onramp.main.entity
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
interface OnrampIntents {
fun onAmountValueChanged(value: String)
fun onAmountValueChanged(value: String, isValuePasted: Boolean)
fun openSettings()
fun openCurrenciesList()
fun onBuyClick(quote: OnrampProviderWithQuote.Data)

View file

@ -1,8 +1,10 @@
package com.tangem.features.onramp.main.entity
import com.tangem.domain.onramp.model.OnrampAmount
import com.tangem.domain.onramp.model.OnrampPaymentMethod
data class OnrampLastUpdate(
val lastAmount: OnrampAmount,
val lastCountryString: String,
val fromAmount: OnrampAmount,
val countryCode: String,
val paymentMethod: OnrampPaymentMethod,
)

View file

@ -122,7 +122,7 @@ internal class OnrampStateFactory(
amountFieldModel = AmountFieldModel(
value = "",
fiatValue = "",
onValueChange = onrampIntents::onAmountValueChanged,
onValueChange = { onrampIntents.onAmountValueChanged(value = it, isValuePasted = false) },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.None,
keyboardType = KeyboardType.Number,

View file

@ -15,9 +15,12 @@ import java.math.BigDecimal
internal class OnrampAmountFieldChangeConverter(
private val currentStateProvider: Provider<OnrampMainComponentUM>,
) : Converter<String, OnrampMainComponentUM> {
) : Converter<OnrampAmountFieldChangeConverter.Input, OnrampMainComponentUM> {
override fun convert(input: Input): OnrampMainComponentUM {
val value = input.value
val isValuePasted = input.isValuePasted
override fun convert(value: String): OnrampMainComponentUM {
val state = currentStateProvider()
if (state !is OnrampMainComponentUM.Content) return state
@ -34,6 +37,7 @@ internal class OnrampAmountFieldChangeConverter(
imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None,
keyboardType = KeyboardType.Number,
),
isValuePasted = isValuePasted,
)
return state.copy(
@ -66,4 +70,6 @@ internal class OnrampAmountFieldChangeConverter(
providerBlockState = OnrampProviderBlockUM.Empty,
)
}
data class Input(val value: String, val isValuePasted: Boolean)
}

View file

@ -33,7 +33,8 @@ internal class OnrampAmountStateFactory(
currentStateProvider = currentStateProvider,
)
fun getOnAmountValueChange(value: String) = onrampAmountFieldChangeConverter.convert(value)
fun getOnAmountValueChange(value: String, isValuePasted: Boolean) =
onrampAmountFieldChangeConverter.convert(OnrampAmountFieldChangeConverter.Input(value, isValuePasted))
fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM {
val currentState = currentStateProvider()

View file

@ -32,6 +32,7 @@ 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.model.EUR_CURRENCY
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -68,7 +69,7 @@ internal class OnrampMainComponentModel @Inject constructor(
private val params: OnrampMainComponent.Params = paramsContainer.require()
private var isSepaLaunched = false
private var shouldForceChooseSepa = params.launchSepa
private var currencyToRestore: OnrampCurrency? = null
val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
@ -132,6 +133,10 @@ internal class OnrampMainComponentModel @Inject constructor(
fun onProviderSelected(result: SelectProviderResult, isBestRate: Boolean) {
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(result, isBestRate) }
if (result.paymentMethod.id != SEPA_METHOD_ID) {
shouldForceChooseSepa = false
}
}
private fun checkResidenceCountry() {
@ -171,7 +176,7 @@ internal class OnrampMainComponentModel @Inject constructor(
updatePairsAndQuotes()
if (wasInitialLoading && params.launchSepa) {
onAmountValueChanged(PREDEFINED_SEPA_AMOUNT)
onAmountValueChanged(value = PREDEFINED_SEPA_AMOUNT, isValuePasted = true)
}
},
)
@ -243,8 +248,8 @@ internal class OnrampMainComponentModel @Inject constructor(
.launchIn(modelScope)
}
override fun onAmountValueChanged(value: String) {
_state.update { amountStateFactory.getOnAmountValueChange(value) }
override fun onAmountValueChanged(value: String, isValuePasted: Boolean) {
_state.update { amountStateFactory.getOnAmountValueChange(value, isValuePasted) }
modelScope.launch { amountInputManager.update(value) }
}
@ -341,9 +346,7 @@ 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
val bestSepaQuote = if (params.launchSepa && shouldForceChooseSepa) {
quotes.filterIsInstance<OnrampQuote.Data>()
.filter { it.paymentMethod.id == SEPA_METHOD_ID }
.maxByOrNull { it.toAmount.value }
@ -352,7 +355,7 @@ internal class OnrampMainComponentModel @Inject constructor(
}
// Check if amount, country or currency has changed
val newQuote = sepaQuote ?: if (checkLastInputState(quoteToCheck)) {
val newQuote = bestSepaQuote ?: if (isAmountOrCountryChanged(quoteToCheck)) {
quoteToCheck
} else {
val state = state.value as? OnrampMainComponentUM.Content
@ -371,7 +374,9 @@ internal class OnrampMainComponentModel @Inject constructor(
lastSelectedQuote
}
}
newQuote?.let { updateProvider(newQuote, quotes) }
if (newQuote != null) {
updateProvider(newQuote, quotes)
}
return newQuote
}
@ -380,8 +385,13 @@ internal class OnrampMainComponentModel @Inject constructor(
lastUpdateState.value = OnrampLastUpdate(
quote.fromAmount,
quote.countryCode,
quote.paymentMethod,
)
if (quote.paymentMethod.id != SEPA_METHOD_ID) {
shouldForceChooseSepa = false
}
_state.update {
amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes)
}
@ -447,22 +457,14 @@ internal class OnrampMainComponentModel @Inject constructor(
}
}
private fun checkLastInputState(quote: OnrampQuote?): Boolean {
return lastUpdateState.value?.lastAmount != quote?.fromAmount ||
lastUpdateState.value?.lastCountryString != quote?.countryCode
private fun isAmountOrCountryChanged(quote: OnrampQuote?): Boolean {
return lastUpdateState.value?.fromAmount != quote?.fromAmount ||
lastUpdateState.value?.countryCode != quote?.countryCode
}
private companion object {
const val UPDATE_DELAY = 10_000L
const val SEPA_METHOD_ID = "sepa"
val EUR_CURRENCY = OnrampCurrency(
code = "EUR",
name = "Euro",
unit = "",
precision = 2,
image = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/Currencies/EUR.png",
)
}
}

View file

@ -58,6 +58,7 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
userWalletId = params.userWalletId,
cryptoCurrency = params.cryptoCurrency,
country = config.country,
launchSepa = false,
onDismiss = { model.bottomSheetNavigation.dismiss() },
),
)

View file

@ -348,8 +348,9 @@ internal class OnrampV2MainComponentModel @Inject constructor(
private fun updateProvider(quote: OnrampQuote) {
lastUpdateState.value = OnrampLastUpdate(
quote.fromAmount,
quote.countryCode,
fromAmount = quote.fromAmount,
countryCode = quote.countryCode,
paymentMethod = quote.paymentMethod,
)
_state.update {
@ -378,8 +379,8 @@ internal class OnrampV2MainComponentModel @Inject constructor(
}
private fun checkLastInputState(quote: OnrampQuote?): Boolean {
return lastUpdateState.value?.lastAmount != quote?.fromAmount ||
lastUpdateState.value?.lastCountryString != quote?.countryCode
return lastUpdateState.value?.fromAmount != quote?.fromAmount ||
lastUpdateState.value?.countryCode != quote?.countryCode
}
private fun sendScreenOpenAnalytics() {

View file

@ -0,0 +1,11 @@
package com.tangem.features.onramp.utils.model
import com.tangem.domain.onramp.model.OnrampCurrency
internal val EUR_CURRENCY = OnrampCurrency(
code = "EUR",
name = "Euro",
unit = "",
precision = 2,
image = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/Currencies/EUR.png",
)

View file

@ -510,7 +510,7 @@ internal class SendConfirmModel @Inject constructor(
val txUrl = getExplorerTransactionUrlUseCase(
txHash = txData.hash.orEmpty(),
networkId = cryptoCurrency.network.id,
).getOrElse { "" }
).getOrNull().orEmpty()
_uiState.update(SendConfirmSentStateTransformer(txData, txUrl))
}

View file

@ -76,7 +76,7 @@ internal class SendConfirmSuccessModel @Inject constructor(
},
),
prevButton = null,
secondaryPairButtonsUM = NavigationButton(
secondaryPairButtonsUM = (NavigationButton(
textReference = resourceReference(R.string.common_explore),
iconRes = R.drawable.ic_web_24,
onClick = ::onExploreClick,
@ -84,7 +84,7 @@ internal class SendConfirmSuccessModel @Inject constructor(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_share_24,
onClick = ::onShareClick,
),
)).takeIf { params.txUrl.isNotEmpty() },
),
),
)

View file

@ -498,7 +498,7 @@ internal class NFTSendConfirmModel @Inject constructor(
iconRes = R.drawable.ic_share_24,
onClick = ::onShareClick,
)
).takeIf { confirmUM is ConfirmUM.Success },
).takeUnless { (confirmUM as? ConfirmUM.Success)?.txUrl.isNullOrBlank() },
),
),
)

View file

@ -52,7 +52,7 @@ internal class SendWithSwapSuccessModel @Inject constructor(
onClick = appRouter::pop,
),
prevButton = null,
secondaryPairButtonsUM = NavigationButton(
secondaryPairButtonsUM = (NavigationButton(
textReference = resourceReference(R.string.common_explore),
iconRes = R.drawable.ic_web_24,
onClick = ::onExploreClick,
@ -60,7 +60,7 @@ internal class SendWithSwapSuccessModel @Inject constructor(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_share_24,
onClick = ::onShareClick,
),
)).takeUnless { confirmUM?.txUrl.isNullOrBlank() },
),
)
}

View file

@ -410,7 +410,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
rateType = RateType.FLOAT,
)
return if (isBalanceWithoutFeeEnough) {
return if (isBalanceWithoutFeeEnough && maybeQuotes.isRight()) {
provider to loadDexSwapData(
provider = provider,
networkId = networkId,

View file

@ -32,6 +32,7 @@ import com.tangem.domain.transaction.error.SendTransactionError.UserCancelledErr
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.WcAnalyticEvents.SignatureRequestReceived.EmulationStatus
import com.tangem.domain.walletconnect.WcAnalyticEvents.SolanaLargeTransaction
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcRequestError.Companion.message
@ -143,6 +144,7 @@ internal class WcSendTransactionModel @Inject constructor(
wcApproval = useCase as? WcApproval
sign = {
if (isMultipleSignRequired(useCase)) {
analytics.send(SolanaLargeTransaction(useCase.rawSdkRequest.dAppMetaData.name))
openMultipleTransaction(useCase)
} else {
useCase.sign()