Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-22 19:02:07 +03:00
commit 4f69db901e
100 changed files with 1875 additions and 378 deletions

View file

@ -33,6 +33,9 @@ class TapWalletManager(
.apply { join() }
}
/**
* [REDACTED_TODO_COMMENT]
*/
private suspend fun loadUserWalletData(userWallet: UserWallet) {
val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy)
trackingContextProxy.setContext(userWallet)

View file

@ -6,7 +6,6 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.keyboard.KeyboardValidator
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.TechAnalyticsEvent
import com.tangem.core.analytics.utils.TrackingContextProxy
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.R
@ -81,7 +80,6 @@ internal class MainViewModel @Inject constructor(
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val appRouterConfig: AppRouterConfig,
private val sellService: SellService,
private val trackingContextProxy: TrackingContextProxy,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -143,7 +141,7 @@ internal class MainViewModel @Inject constructor(
}
}
prepareSelectedWalletFeedback()
subscribeToSelectedWallet()
// await while initial route stack is initialized
appRouterConfig.initializedState.first { it }
@ -172,12 +170,15 @@ internal class MainViewModel @Inject constructor(
}
}
private fun prepareSelectedWalletFeedback() {
private fun subscribeToSelectedWallet() {
getSelectedWalletUseCase.invoke()
.mapLeft { emptyFlow<UserWallet>() }
.onRight { wallet ->
wallet.distinctUntilChanged()
.onEach { trackingContextProxy.setContext(it) }
.onEach {
// FIXME Do not remove this call without checking implications !!!
appStateHolder.onUserWalletSelected(it)
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}

View file

@ -1,20 +1,15 @@
package com.tangem.common
import com.tangem.utils.SupportedLanguages
/**
[REDACTED_AUTHOR]
*/
object TangemBlogUrlBuilder {
fun build(post: Post): String {
val code = SupportedLanguages.getCurrentSupportedLanguageCode()
.takeIf { code ->
code == SupportedLanguages.RUSSIAN || code == SupportedLanguages.ENGLISH
}
?: SupportedLanguages.ENGLISH
return "https://tangem.com/$code/blog/post/${post.path}/"
suspend fun build(post: Post): String {
return TangemSiteUrlBuilder.url(
path = "/blog/post/${post.path}/",
campaign = "articles",
)
}
sealed interface Post {
@ -28,5 +23,13 @@ object TangemBlogUrlBuilder {
data object SeedNotifySecond : Post {
override val path: String = "tangem-resolves-log-issue"
}
data object SeedPhraseRiskySolution : Post {
override val path: String = "seed-phrase-a-risky-solution"
}
data object WhatWalletToChoose : Post {
override val path: String = "mobile-wallet"
}
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.common
import android.content.res.Resources
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import java.util.Locale
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object TangemSiteUrlBuilder {
suspend fun getUtmTags(campaign: String?): String {
val langCode = Locale.getDefault().language
val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty()
val utmContent = deviceLang()?.let { "&utm_content=devicelang-$it" }.orEmpty()
val appInstanceIdPart = getAppInstanceId()?.let { "&app_instance_id=$it" }.orEmpty()
return "utm_source=tangem-app&utm_medium=app$utmCampaignPart$utmContent$appInstanceIdPart"
}
suspend fun url(path: String, campaign: String): String {
val normalizedPath = path.trim('/')
return "https://tangem.com/$normalizedPath?${getUtmTags(campaign)}"
}
private fun deviceLang(): String? {
return runCatching {
Resources.getSystem().configuration.locales.get(0).toLanguageTag()
}.getOrNull()
}
private suspend fun getAppInstanceId(): String? {
return suspendCoroutine { cont ->
Firebase.analytics.appInstanceId
.addOnSuccessListener { id ->
cont.resume(id)
}
.addOnFailureListener {
cont.resume(null)
}
}
}
}

View file

@ -403,6 +403,9 @@ class TokenItemStateConverter(
onYieldPromoCloseClick: (() -> Unit)?,
): TokenItemState.PromoBannerState {
val token = status.currency as? CryptoCurrency.Token ?: return TokenItemState.PromoBannerState.Empty
if (status.value !is CryptoCurrencyStatus.Loaded) {
return TokenItemState.PromoBannerState.Empty
}
if (yieldSupplyPromoBannerKey == null || yieldSupplyPromoBannerKey != token.yieldSupplyKey() ||
yieldModuleApyMap[token.yieldSupplyKey()] == null
) {

View file

@ -18,6 +18,54 @@ sealed class ApiResponseError : Exception() {
val errorBody: String?,
) : ApiResponseError() {
fun isServerError(): Boolean = when (code) {
Code.OK,
Code.CREATED,
Code.ACCEPTED,
Code.NOT_MODIFIED,
Code.BAD_REQUEST,
Code.UNAUTHORIZED,
Code.PAYMENT_REQUIRED,
Code.FORBIDDEN,
Code.NOT_FOUND,
Code.METHOD_NOT_ALLOWED,
Code.NOT_ACCEPTABLE,
Code.PROXY_AUTHENTICATION_REQUIRED,
Code.REQUEST_TIMEOUT,
Code.CONFLICT,
Code.GONE,
Code.LENGTH_REQUIRED,
Code.PRECONDITION_FAILED,
Code.PAYLOAD_TOO_LARGE,
Code.URI_TOO_LONG,
Code.UNSUPPORTED_MEDIA_TYPE,
Code.RANGE_NOT_SATISFIABLE,
Code.EXPECTATION_FAILED,
Code.IM_A_TEAPOT,
Code.UNPROCESSABLE_ENTITY,
Code.LOCKED,
Code.FAILED_DEPENDENCY,
Code.TOO_EARLY,
Code.UPGRADE_REQUIRED,
Code.PRECONDITION_REQUIRED,
Code.TOO_MANY_REQUESTS,
Code.REQUEST_HEADER_FIELDS_TOO_LARGE,
Code.UNAVAILABLE_FOR_LEGAL_REASONS,
-> false
Code.INTERNAL_SERVER_ERROR,
Code.NOT_IMPLEMENTED,
Code.BAD_GATEWAY,
Code.SERVICE_UNAVAILABLE,
Code.GATEWAY_TIMEOUT,
Code.HTTP_VERSION_NOT_SUPPORTED,
Code.VARIANT_ALSO_NEGOTIATES,
Code.INSUFFICIENT_STORAGE,
Code.LOOP_DETECTED,
Code.NOT_EXTENDED,
Code.NETWORK_AUTHENTICATION_REQUIRED,
-> true
}
// TODO: extract Code from HttpException
// region Error Codes
enum class Code(val numericCode: Int) {

View file

@ -4,5 +4,4 @@ import com.squareup.moshi.Json
data class CardBalanceResponse(
@Json(name = "result") val result: BalanceResponse?,
@Json(name = "error") val error: String?,
)

View file

@ -5,7 +5,6 @@ import com.squareup.moshi.JsonClass
data class CardDetailsResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@ -18,6 +17,7 @@ data class CardDetailsResponse(
@Json(name = "card_number_end") val cardNumberEnd: String,
@Json(name = "pan") val pan: Secret,
@Json(name = "cvv") val cvv: Secret,
@Json(name = "pin") val pin: Secret?,
)
@JsonClass(generateAdapter = true)

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CheckCustomerWalletResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
data class Result(
@Json(name = "id") val id: String?,

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CustomerEligibilityResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class CustomerMeResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@ -97,5 +96,6 @@ data class CustomerMeResponse(
@Json(name = "card_type") val cardType: String,
@Json(name = "card_status") val cardStatus: String,
@Json(name = "card_number_end") val cardNumberEnd: String,
@Json(name = "is_pin_set") val isPinSet: Boolean?,
)
}

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class DeeplinkValidityResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class FreezeUnfreezeCardResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class OrderResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

@ -7,7 +7,6 @@ import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class TangemPayTxHistoryResponse(
@Json(name = "error") val error: String?,
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WithdrawDataResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

@ -6,7 +6,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class WithdrawResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(

View file

@ -896,6 +896,8 @@
<string name="reset_card_without_backup_to_factory_message">La réinitialisation aux paramètres d\'usine supprimera complètement le portefeuille de la carte sélectionnée et le supprimera de l\'application. Vous ne pourrez pas restaurer le portefeuille actuel.</string>
<string name="ring_promo_text">Les propriétaires du Tangem Ring ont droit à 3 swap gratis sur Changelly jusqu\'au 15/11 !</string>
<string name="ring_promo_title">Échangez avec 0 % de frais !</string>
<string name="root_detected_warning_description">Les appareils disposant d\'un accès root sont considérés comme moins sécurisés. Vos données peuvent être exposées à des risques supplémentaires.</string>
<string name="root_detected_warning_title">Accès root détecté</string>
<string name="save_user_wallet_agreement_access_description">Connectez-vous à l\'application et vérifiez votre solde sans scanner la carte</string>
<string name="save_user_wallet_agreement_access_title">Accéder à l\'application</string>
<string name="save_user_wallet_agreement_allow_biometrics">Autoriser l\'utilisation de la biométrie</string>

View file

@ -103,10 +103,10 @@
<string name="app_settings_enable_biometrics_description">設定に移動して、Tangemアプリで生体認証を有効にします。</string>
<string name="app_settings_enable_biometrics_title">生体認証を有効にする</string>
<string name="app_settings_off_biometrics_alert_message">%1$sが無効になると、アプリのロックを解除してウォレットを操作するために、パスコードを入力する必要があります。</string>
<string name="app_settings_off_require_access_code_alert_message">後でウォレットのアクセスコードを入力してもらいます。それを安全に保存し、今後の利用に備えるためです。</string>
<string name="app_settings_off_saved_access_code_alert_message">これにより、保存されているウォレットアクセスコードがすべて削除されます。ウォレットでの今後の操作には、アクセスコードの送信が必要になります。</string>
<string name="app_settings_off_require_access_code_alert_message">安全に保管するため、後ほどアクセスコードの入力を求められます。</string>
<string name="app_settings_off_saved_access_code_alert_message">保存されているすべてのウォレットのアクセスコードが削除されます。ウォレットを使用するには、再度アクセスコードを入力する必要があります。</string>
<string name="app_settings_off_saved_wallet_alert_message">保存したデバイスを削除すると、保存されているすべてのウォレットとそのアクセスコードがアプリから削除されます。</string>
<string name="app_settings_on_require_access_code_alert_message">これにより、保存されているウォレットアクセスコードがすべて削除されます。今後ウォレットを操作するには、アクセスコードの送信が必要になります。</string>
<string name="app_settings_on_require_access_code_alert_message">保存されているすべてのウォレットのアクセスコードが削除されます。ウォレットを使用するには、再度アクセスコードを入力する必要があります。</string>
<string name="app_settings_require_access_code">アクセスコードを要求する</string>
<string name="app_settings_require_access_code_footer">このオプションを選択すると、機密性の高い操作における生体認証が無効になります。取引に署名するたびにアクセスコードを入力する必要があります。</string>
<string name="app_settings_saved_access_codes">アクセスコードを保存</string>
@ -144,10 +144,10 @@
<string name="balance_hidden_title">残高は非表示</string>
<string name="beta_mode_warning_message">ブロックチェーン開発者によると、Kaspaトークンは現在ベータ版です。アップデートをお楽しみに</string>
<string name="beta_mode_warning_title">ベータモード</string>
<string name="biometric_disabled_warning_description">デバイスで生体認証がオフになっているため、ウォレットのロック解除に使用できません。生体認証を再度利用するには、デバイスの設定で有効にしてください。</string>
<string name="biometric_disabled_warning_description">この端末では生体認証がオフになっているため、ウォレットの解除に使用できません。再度この方法を使用するには、端末の設定で生体認証を有効にしてください。</string>
<string name="biometric_disabled_warning_title">生体認証が無効になっています</string>
<string name="biometric_lockout_permanent_warning_description">カードまたはリングをスキャンしてください</string>
<string name="biometric_lockout_permanent_warning_description_2">生体認証の試行回数が上限に達しました。端末タップまたはアクセスコードでウォレットを解除してください。</string>
<string name="biometric_lockout_permanent_warning_description_2">生体認証の試行回数の上限に達しました。カード/リングでウォレットを解除するか、アクセスコードを入力してください。</string>
<string name="biometric_lockout_permanent_warning_title">生体認証がロックされています</string>
<string name="biometric_lockout_warning_description">30秒後に再試行するか、カードまたはリングをスキャンしてください</string>
<string name="biometric_lockout_warning_description_2">生体認証ログインは一時的にロックされています。30秒後にもう一度お試しいただくか、端末タップまたはアクセスコードでウォレットを解除してください。</string>
@ -179,7 +179,7 @@
<string name="card_reset_alert_finish_ok_button">もう一度アップグレードしてください</string>
<string name="card_reset_alert_finish_title">リセット完了</string>
<string name="card_reset_alert_incomplete_message">このウォレット内のすべてのTangemデバイスで、リセット処理を完了することを推奨します。</string>
<string name="card_reset_alert_incomplete_title">すべてのTangemデバイスをリセットしていません</string>
<string name="card_reset_alert_incomplete_title">リセットが必要なTangemデバイスがあります。</string>
<string name="card_settings_access_code_recovery_disabled_description">このカードを使用して、このウォレット内の他のカードまたはリングのアクセスコードをリセットしたくない場合は、このオプションを無効にしてください。これにより、このカードのアクセスコードもリセットできなくなりますのでご注意ください。</string>
<string name="card_settings_access_code_recovery_enabled_description">このカードを使用して、このウォレット内の他のカードのアクセスコードをリセットできます。</string>
<string name="card_settings_access_code_recovery_title">アクセスコードの復元</string>
@ -378,6 +378,7 @@
<string name="common_transfer">送金</string>
<string name="common_unable_to_load">データを読み込めません…</string>
<string name="common_understand">わかりました</string>
<string name="common_understand_continue">理解して続行</string>
<string name="common_unknown_error">エラーが発生しました。もう一度お試しください。</string>
<string name="common_unreachable">アクセスできません</string>
<string name="common_unstake">ステーキング解除</string>
@ -567,7 +568,7 @@
<string name="home_button_create_new_wallet">新しいウォレットを作成する</string>
<string name="home_button_order">Tangemを注文</string>
<string name="home_button_scan">Tangemをスキャン</string>
<string name="hot_access_code_set_biometric_ask">「Tangem」に生体認証の使用を許可しますか?\n本人確認とアプリの起動のために使用されます。</string>
<string name="hot_access_code_set_biometric_ask">「Tangem」が生体認証を使用して本人確認を行い、アプリを開くことを許可しますか?</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>
@ -1059,6 +1060,7 @@
<string name="reset_card_to_factory_button_title">カードをリセットする</string>
<string name="reset_card_to_factory_condition_1">この操作を実行すると、現在のウォレットにアクセスできなくなることを理解しています。</string>
<string name="reset_card_to_factory_condition_2">このカードを使用して、現在のウォレットの他のカードのアクセスコードを回復させられないことを認識しています。</string>
<string name="reset_card_to_factory_condition_3">復元は不可能であり、Tangem Payカードおよびカード上のすべての資金へのアクセスを完全に失うことを理解しました</string>
<string name="reset_card_with_backup_to_factory_message">工場出荷時設定にリセットすると、選択したカードやリングからウォレットが完全に削除されます。現在のウォレットを復元したり、カードやリングを使用してアクセスコードを復元することはできません。</string>
<string name="reset_card_without_backup_to_factory_message">工場出荷時の状態にリセットすると、選択したカードやリングからウォレットが完全に削除され、アプリから削除されます。現在のウォレットを復元することはできません。</string>
<string name="reset_cards_dialog_complete_description">すべてのTangemデバイスがリセットされました。</string>
@ -1067,6 +1069,8 @@
<string name="reset_cards_dialog_next_device_description">続行するには次のデバイスをリセットしてください</string>
<string name="ring_promo_text">Tangem Ringユーザーは、11/15日までChangelly経由でスワップを3回手数料ゼロで行えます</string>
<string name="ring_promo_title">今すぐ手数料0% でスワップしましょう!</string>
<string name="root_detected_warning_description">Rootアクセスが有効な端末は、セキュリティが低いと判断されます。データが追加のリスクにさらされる可能性があります。</string>
<string name="root_detected_warning_title">Rootアクセスが検出されました</string>
<string name="save_user_wallet_agreement_access_description">アプリにログインして、カードまたはリングをスキャンせずに残高を確認できます</string>
<string name="save_user_wallet_agreement_access_title">アプリにアクセスする</string>
<string name="save_user_wallet_agreement_allow_biometrics">生体認証の使用を許可する</string>
@ -1402,6 +1406,7 @@
<string name="tangem_pay_freeze_card_success">カードが凍結されています</string>
<string name="tangem_pay_get_help">サポートを受ける</string>
<string name="tangem_pay_other">その他</string>
<string name="tangem_pay_rooted_device_subtitle">Root化された端末では使用できません</string>
<string name="tangem_pay_status_completed">完了</string>
<string name="tangem_pay_status_declined">拒否</string>
<string name="tangem_pay_status_pending">保留中</string>
@ -1414,6 +1419,8 @@
<string name="tangem_pay_unfreeze_card_failed">カードの凍結解除に失敗しました。しばらくしてからもう一度お試しください。</string>
<string name="tangem_pay_unfreeze_card_success">カードの凍結が解除されました</string>
<string name="tangem_pay_withdrawal">出金</string>
<string name="tangempay_account_unable_to_use_rooted">Root化された端末では使用できません</string>
<string name="tangempay_cancel_kyc">KYCをキャンセル</string>
<string name="tangempay_card_details_add_funds">資金を追加</string>
<string name="tangempay_card_details_add_funds_subtitle">入金オプション</string>
<string name="tangempay_card_details_card_number">カード番号</string>
@ -1441,6 +1448,7 @@
<string name="tangempay_card_details_open_wallet_step_5">準備完了!カードはすぐに使用できます。</string>
<string name="tangempay_card_details_open_wallet_title">Google Payにカードを追加する</string>
<string name="tangempay_card_details_open_wallet_title_apple">Apple Payにカードを追加する</string>
<string name="tangempay_card_details_pin_code">PINコード</string>
<string name="tangempay_card_details_receive_description">アドレスを共有するか、QRコードを表示してください。</string>
<string name="tangempay_card_details_receive_error_description">技術的な問題が検出されました。しばらくしてからもう一度お試しいただくか、サポートにお問い合わせください。</string>
<string name="tangempay_card_details_receive_error_title">現在、受け取りは利用できません</string>
@ -1449,12 +1457,15 @@
<string name="tangempay_card_details_swap_description">ポートフォリオ内のあらゆる資産をカードと交換</string>
<string name="tangempay_card_details_title">カードの詳細</string>
<string name="tangempay_card_details_unfreeze_card">カードの一時停止を解除</string>
<string name="tangempay_card_details_view_pin_code_description">忘れた場合は、アプリに戻ってください。</string>
<string name="tangempay_card_details_view_pin_code_title">PINコード</string>
<string name="tangempay_card_details_withdraw">出金</string>
<string name="tangempay_card_details_withdraw_error_title">現在、出金ができません</string>
<string name="tangempay_card_details_withdraw_in_progress_description">現在の処理が完了するまでは、スワップや新しい出金を開始することはできません。</string>
<string name="tangempay_card_details_withdraw_in_progress_title">出金処理中です</string>
<string name="tangempay_change_pin_code">PINコードを変更</string>
<string name="tangempay_come_back_if_forget_pin">忘れた場合はアプリに戻って確認できます。</string>
<string name="tangempay_factory_settings_warning_title">復元は不可能であり、Tangem Payカードおよびカード上のすべての資金へのアクセスを完全に失うことを理解しました</string>
<string name="tangempay_failed_to_issue_card">カードの発行に失敗しました</string>
<string name="tangempay_failed_to_issue_card_retry_description">技術的なエラーが発生しました。下のボタンをクリックして、もう一度お試しください。</string>
<string name="tangempay_failed_to_issue_card_support_description">技術的なエラーが発生しました。サポートへお問い合わせください。</string>
@ -1471,6 +1482,7 @@
<string name="tangempay_kyc_in_progress">KYC進行中</string>
<string name="tangempay_kyc_in_progress_notification_button">ステータスを表示</string>
<string name="tangempay_kyc_in_progress_notification_title">Tangem PayのKYC手続き進行中</string>
<string name="tangempay_kyc_in_progress_popup_description">以下のボタンから、現在のKYCステータスを確認するか、KYCをキャンセルできます。</string>
<string name="tangempay_onboarding_banner_description">暗号資産を、リアルな支払いに。\n他とはまったく違う、新しいタイプの決済カード。</string>
<string name="tangempay_onboarding_banner_title">Tangem Visaカード</string>
<string name="tangempay_onboarding_get_card_button_text">カードをGET</string>
@ -1488,7 +1500,7 @@
<string name="tangempay_service_unreachable_try_later">現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。</string>
<string name="tangempay_sync_needed">同期が必要です</string>
<string name="tangempay_tangem_visa_card">Tangem Visaカード</string>
<string name="tangempay_temporarily_unavailable">Tangem Payは一時的に利用できません</string>
<string name="tangempay_temporarily_unavailable">Tangem Payは現在一時的に利用できません</string>
<string name="tangempay_title">Tangem Pay</string>
<string name="tangempay_use_tangem_device_to_restore_payment_account">カードまたはリングを使用して、支払いアカウントへのアクセスを復元してください。</string>
<string name="tangempay_your_pin_code">PINコード</string>
@ -1572,7 +1584,7 @@
<string name="user_push_notification_agreement_header">プッシュ通知を使用しますか?</string>
<string name="user_push_notification_banner_subtitle">プッシュ通知を有効にすると、ウォレットに着金したときにアラートを受信できます。</string>
<string name="user_push_notification_banner_title">取引を見逃さない</string>
<string name="user_wallet_list_add_button">新しいウォレットを追加</string>
<string name="user_wallet_list_add_button">ウォレットを追加</string>
<string name="user_wallet_list_delete_hw_prompt">バックアップせずにこのウォレットを削除すると、資金に永久にアクセスできなくなります。</string>
<string name="user_wallet_list_delete_prompt">このウォレットを忘れてもよろしいですか?</string>
<string name="user_wallet_list_error_unable_to_unlock">エラーが発生しました。カードまたはリングをスキャンしてログインしてください。</string>
@ -1879,7 +1891,7 @@
<string name="wc_warning_transaction">不審な取引</string>
<string name="welcome_create_wallet_already_have">すでにTangemウォレットをお持ちですか</string>
<string name="welcome_create_wallet_feature_assets">数千種類の資産</string>
<string name="welcome_create_wallet_feature_class">業界最高水準のハードウェアウォレット</string>
<string name="welcome_create_wallet_feature_class">最高水準のハードウェアウォレット</string>
<string name="welcome_create_wallet_feature_delivery">迅速な配送</string>
<string name="welcome_create_wallet_feature_one_tap">ワンタップで開始</string>
<string name="welcome_create_wallet_feature_seamless">シームレスで安全</string>

View file

@ -394,6 +394,7 @@
<string name="common_transfer">Перевод</string>
<string name="common_unable_to_load">Невозможно загрузить данные…</string>
<string name="common_understand">Я понял</string>
<string name="common_understand_continue">Я понимаю, продолжить</string>
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
<string name="common_unreachable">Недоступно</string>
<string name="common_unstake">Завершить стейкинг</string>
@ -733,6 +734,7 @@
<string name="markets_sort_by_top_gainers_title">Лидеры роста</string>
<string name="markets_sort_by_top_losers_title">Лидеры падения</string>
<string name="markets_sort_by_trending_title">В тренде</string>
<string name="markets_sort_by_yield_mode_title">Режим доходности</string>
<string name="markets_staking_banner_description_placeholder">Стейкинг — простой способ получать доход с вашей криптовалюты. %s</string>
<string name="markets_staking_banner_title">Получайте до %s APY</string>
<string name="markets_token_added">Токен добавлен</string>
@ -994,6 +996,7 @@
<string name="onramp_currency_other">Другие валюты</string>
<string name="onramp_currency_popular">Популярные фиаты</string>
<string name="onramp_currency_search">Поиск по валюте</string>
<string name="onramp_error_transaction_already_processed">Эта транзакция уже была обработана. Дополнительных действий не требуется.</string>
<string name="onramp_fetching_best_rates">Получение лучших курсов...</string>
<string name="onramp_instant_status">Моментально</string>
<string name="onramp_legal">Пользуясь сервисом покупки, вы соглашаетесь с %1$s и %2$s</string>
@ -1104,6 +1107,8 @@
<string name="reset_cards_dialog_next_device_description">Сбросьте следующее устройство для продолжения.</string>
<string name="ring_promo_text">Владельцам колец — 3 обмена без комиссии на Changelly до 15.11!</string>
<string name="ring_promo_title">Обмен с 0% комиссией!</string>
<string name="root_detected_warning_description">Устройства с root-доступом считаются менее безопасными. Ваши данные могут быть подвержены дополнительным рискам.</string>
<string name="root_detected_warning_title">Обнаружен root-доступ</string>
<string name="save_user_wallet_agreement_access_description">Войдите в приложение и следите за своим балансом без сканирования карты или кольца</string>
<string name="save_user_wallet_agreement_access_title">Доступ в приложение</string>
<string name="save_user_wallet_agreement_allow_biometrics">Использовать биометрию</string>
@ -1493,6 +1498,7 @@
<string name="tangempay_card_details_withdraw_in_progress_title">Вывод выполняется</string>
<string name="tangempay_change_pin_code">Изменить PIN-код</string>
<string name="tangempay_come_back_if_forget_pin">Можно посмотреть здесь, если забудете его.</string>
<string name="tangempay_factory_settings_warning_title">Я понимаю, что полностью потеряю доступ к своей карте Tangem Pay и ко всем средствам на ней без возможности восстановления.</string>
<string name="tangempay_failed_to_issue_card">Не удалось выпустить карту</string>
<string name="tangempay_failed_to_issue_card_retry_description">Техническая ошибка, попробуйте ещё раз, нажав кнопку ниже</string>
<string name="tangempay_failed_to_issue_card_support_description">Техническая ошибка, свяжитесь с поддержкой</string>
@ -1964,6 +1970,7 @@
<string name="yield_module_fee_policy_sheet_title">Политика комиссий за пополнение</string>
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangem взимает комиссию за обслуживание в размере 15% от полученного дохода.</string>
<string name="yield_module_high_fee_error">Ваши средства будут автоматически переведены в Aave, как только комиссия сети снизится или баланс достигнет минимально необходимой суммы.</string>
<string name="yield_module_high_network_fees_notification_description">Комиссии сейчас выше обычного из-за высокой активности на рынке. Вы можете продолжить сейчас или вернуться позже, когда комиссии снизятся.</string>
<string name="yield_module_high_network_fees_notification_title">Высокая сетевая комиссия</string>
<string name="yield_module_historical_returns">Историческая доходность</string>
<string name="yield_module_main_screen_promo_banner_message">Получай до %1$s APY на свой баланс</string>

View file

@ -1444,6 +1444,7 @@
<string name="tangem_pay_unfreeze_card_success">Your card is unfrozen.</string>
<string name="tangem_pay_withdrawal">Withdrawal</string>
<string name="tangempay_account_unable_to_use_rooted">Unable to use on rooted device</string>
<string name="tangempay_cancel_kyc">Cancel KYC</string>
<string name="tangempay_card_details_add_funds">Add funds</string>
<string name="tangempay_card_details_add_funds_subtitle">Top-up options</string>
<string name="tangempay_card_details_card_number">Card Number</string>
@ -1471,6 +1472,7 @@
<string name="tangempay_card_details_open_wallet_step_5">All set! Your card is ready to use.</string>
<string name="tangempay_card_details_open_wallet_title">Add card to Google Pay</string>
<string name="tangempay_card_details_open_wallet_title_apple">Add card to Apple Pay</string>
<string name="tangempay_card_details_pin_code">PIN code</string>
<string name="tangempay_card_details_receive_description">Share your address or show QR-code</string>
<string name="tangempay_card_details_receive_error_description">Technical issues detected. Please try again later or contact support.</string>
<string name="tangempay_card_details_receive_error_title">Receive unavailable now</string>
@ -1479,6 +1481,8 @@
<string name="tangempay_card_details_swap_description">Swap any asset in your portfolio for card</string>
<string name="tangempay_card_details_title">Card details</string>
<string name="tangempay_card_details_unfreeze_card">Unfreeze Card</string>
<string name="tangempay_card_details_view_pin_code_description">Come back to the app if you forget it.</string>
<string name="tangempay_card_details_view_pin_code_title">Your PIN code</string>
<string name="tangempay_card_details_withdraw">Withdraw</string>
<string name="tangempay_card_details_withdraw_error_title">Withdraw unavailable now</string>
<string name="tangempay_card_details_withdraw_in_progress_description">You can\'t initiate swap or new withdrawal till the current one is finished</string>
@ -1502,6 +1506,7 @@
<string name="tangempay_kyc_in_progress">KYC in progress</string>
<string name="tangempay_kyc_in_progress_notification_button">View Status</string>
<string name="tangempay_kyc_in_progress_notification_title">KYC in progress for Tangem Pay</string>
<string name="tangempay_kyc_in_progress_popup_description">Use the buttons below to view your current KYC status or cancel it.</string>
<string name="tangempay_onboarding_banner_description">Use your crypto for real world spending. \nIts a payment card unlike any other.</string>
<string name="tangempay_onboarding_banner_title">Tangem Visa Card</string>
<string name="tangempay_onboarding_get_card_button_text">Get card</string>

View file

@ -44,8 +44,8 @@ internal fun YieldSupplyPromoBanner(state: PromoBannerState.Content, modifier: M
Row(
modifier = Modifier
.background(color = bgColor, shape = TangemTheme.shapes.roundedCornersXMedium)
.padding(horizontal = 12.dp, vertical = 8.dp)
.clickable(onClick = state.onPromoBannerClick)
.padding(horizontal = 12.dp, vertical = 8.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {

View file

@ -10,7 +10,7 @@ object SupportedLanguages {
const val ITALIAN = "it"
const val JAPANESE = "ja"
const val UKRAINIAN = "uk"
const val CHINESE = "uk"
const val CHINESE = "zh"
const val SPANISH = "es"
val supportedLangugeCodes = listOf(

View file

@ -2,6 +2,7 @@ package com.tangem.utils
import java.util.Locale
@Deprecated("Use TangemBlogUrlBuilder from common module")
object TangemBlogUrlBuilder {
private const val RU_LOCALE = "ru"

View file

@ -51,7 +51,7 @@ internal class DefaultP2PEthPoolRepository(
emptyList()
}
p2pEthPoolVaultsStore.store(vaults.filter { !it.isPrivate }) // TODO eth isSmoothingPool?
p2pEthPoolVaultsStore.store(vaults)
}
override suspend fun getVaults(network: P2PEthPoolNetwork): Either<StakingError, List<P2PEthPoolVault>> = either {

View file

@ -9,26 +9,43 @@ import com.tangem.domain.pay.TangemPayEligibilityManager
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject
internal class DefaultTangemPayEligibilityManager @Inject constructor(
dispatchers: CoroutineDispatcherProvider,
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val onboardingRepository: OnboardingRepository,
) : TangemPayEligibilityManager {
private var cachedEligibleWallets: List<UserWallet>? = null
private var eligibleWalletsDeferred: Deferred<List<UserWallet>>? = null
private var cachedEligibleWallets: List<UserWalletData>? = null
private var eligibleWalletsDeferred: Deferred<List<UserWalletData>>? = null
private val loadMutex = Mutex()
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default)
override suspend fun getEligibleWallets(): List<UserWallet> {
init {
resetDataWhenWalletsUpdate()
}
override suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List<UserWallet> {
return getUserWalletsData().mapNotNull {
if (!it.isPaeraCustomer || !shouldExcludePaeraCustomers) it.userWallet else null
}
}
private suspend fun getUserWalletsData(): List<UserWalletData> {
cachedEligibleWallets?.let { return it }
return loadMutex.withLock {
@ -38,7 +55,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
coroutineScope {
val deferred = async {
getPossibleWalletsForTangemPay()
.excludePaeraCustomers()
.addPaeraCustomersData()
.also { cachedEligibleWallets = it }
}
eligibleWalletsDeferred = deferred
@ -73,8 +90,8 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
is UserWallet.Hot -> true
}
private suspend fun List<UserWallet>.excludePaeraCustomers(): List<UserWallet> {
if (isEmpty()) return this
private suspend fun List<UserWallet>.addPaeraCustomersData(): List<UserWalletData> {
if (isEmpty()) return emptyList()
return coroutineScope {
map { wallet ->
@ -86,9 +103,30 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
}
}
.awaitAll()
.mapNotNull { (wallet, isCustomer) ->
wallet.takeUnless { isCustomer }
.map { (userWallet, isPaeraCustomer) ->
UserWalletData(userWallet, isPaeraCustomer)
}
}
}
private fun resetDataWhenWalletsUpdate() {
coroutineScope.launch {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.userWallets.collectLatest { reset() }
} else {
userWalletsListManager.userWallets.collectLatest { reset() }
}
}
}
private fun reset() {
cachedEligibleWallets = null
eligibleWalletsDeferred?.cancel()
eligibleWalletsDeferred = null
}
private data class UserWalletData(
val userWallet: UserWallet,
val isPaeraCustomer: Boolean,
)
}

View file

@ -144,6 +144,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
currencyCode = fiatBalance.currency,
customerWalletAddress = paymentAccount.customerWalletAddress,
depositAddress = response.depositAddress,
isPinSet = response.card?.isPinSet == true,
)
} else {
null

View file

@ -82,12 +82,14 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
block = {
val publicKeyBase64 = getPublicKeyBase64()
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
val result = requestHelper.performRequest(userWalletId = userWalletId) { authHeader ->
tangemPayApi.revealCardDetails(
authHeader = authHeader,
body = CardDetailsRequest(sessionId = sessionId),
)
}.getOrNull()?.result ?: error("Cannot reveal card details")
val result = requireNotNull(
requestHelper.performRequest(userWalletId = userWalletId) { authHeader ->
tangemPayApi.revealCardDetails(
authHeader = authHeader,
body = CardDetailsRequest(sessionId = sessionId),
)
}.getOrNull()?.result,
)
val pan = rainCryptoUtil.decryptSecret(
base64Secret = result.pan.secret,
@ -113,6 +115,38 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
)
}
override suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?> {
return catch(
block = {
val publicKeyBase64 = getPublicKeyBase64()
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
val result = requireNotNull(
requestHelper.performRequest(userWalletId = userWalletId) { authHeader ->
tangemPayApi.revealCardDetails(
authHeader = authHeader,
body = CardDetailsRequest(sessionId = sessionId),
)
}.getOrNull()?.result,
)
val encryptedPin = result.pin
val pin = if (encryptedPin != null) {
rainCryptoUtil.decryptPin(
base64Secret = encryptedPin.secret,
base64Iv = encryptedPin.iv,
secretKeyBytes = secretKeyBytes,
).takeIf { !it.isNullOrEmpty() }
} else {
null
}
secretKeyBytes.fill(0)
pin.right()
},
catch = ::catchException,
)
}
override suspend fun setPin(userWalletId: UserWalletId, pin: String): Either<UniversalError, SetPinResult> {
return requestHelper.runWithErrorLogs(TAG) {
val publicKeyBase64 = getPublicKeyBase64()
@ -120,16 +154,18 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
val encryptedData = rainCryptoUtil.encryptPin(pin = pin, secretKeyBytes = secretKeyBytes)
secretKeyBytes.fill(0)
val status = requestHelper.request(userWalletId) { authHeader ->
tangemPayApi.setPin(
authHeader = authHeader,
body = SetPinRequest(
sessionId = sessionId,
pin = encryptedData.encryptedBase64,
iv = encryptedData.ivBase64,
),
)
}.result?.result ?: error("Cannot set pin code")
val status = requireNotNull(
requestHelper.request(userWalletId) { authHeader ->
tangemPayApi.setPin(
authHeader = authHeader,
body = SetPinRequest(
sessionId = sessionId,
pin = encryptedData.encryptedBase64,
iv = encryptedData.ivBase64,
),
)
}.result?.result,
)
when (status) {
SetPinResult.SUCCESS.name -> SetPinResult.SUCCESS
SetPinResult.PIN_TOO_WEAK.name -> SetPinResult.PIN_TOO_WEAK

View file

@ -172,10 +172,11 @@ internal class TangemPayRequestPerformer @Inject constructor(
refreshToken = response.refreshToken,
refreshExpiresAt = response.refreshExpiresAt,
)
}.mapLeft { error ->
Timber.tag(TAG).e("Can not refresh auth tokens: $error")
if (error is VisaApiError.ServerUnavailable) error else VisaApiError.RefreshTokenExpired
}.onRight { tokens ->
tangemPayStorage.storeAuthTokens(customerWalletAddress = customerWalletAddress, tokens = tokens)
}.onLeft {
Timber.tag(TAG).e("Can not refresh auth tokens: $it")
}
}
}

View file

@ -37,6 +37,15 @@ internal class RainCryptoUtil @Inject constructor(
secretKeyBytes to sessionId
}
suspend fun decryptPin(base64Secret: String, base64Iv: String, secretKeyBytes: ByteArray): String? {
val pinBlock = decryptSecret(
base64Secret = base64Secret,
base64Iv = base64Iv,
secretKeyBytes = secretKeyBytes,
)
return extractPinFromPinBlock(pinBlock)
}
suspend fun encryptPin(pin: String, secretKeyBytes: ByteArray): EncryptedData = withContext(dispatchers.default) {
val bytes = pinBlockByteArray(pin)
try {
@ -113,6 +122,17 @@ internal class RainCryptoUtil @Inject constructor(
return hex.toByteArray(StandardCharsets.UTF_8)
}
private suspend fun extractPinFromPinBlock(pinBlock: String): String? = withContext(dispatchers.default) {
val pinLength = pinBlock[1].digitToIntOrNull()
require(pinLength == PIN_LENGTH) { "Unexpected PIN length: ${pinBlock[1]}" }
val pinStartIndex = 2
val pinEndIndex = pinStartIndex + pinLength
val pin = pinBlock.substring(pinStartIndex, pinEndIndex)
pin.takeIf { value -> value.all { it.isDigit() } && value.length == PIN_LENGTH }
}
private fun ByteArray.clear() {
for (i in indices) this[i] = 0
}

View file

@ -18,6 +18,7 @@ internal class TangemPayErrorConverter @Inject constructor(
override fun convert(value: Throwable): VisaApiError {
return if (value is ApiResponseError.HttpException) {
if (value.isServerError()) return VisaApiError.ServerUnavailable
if (value.code == ApiResponseError.HttpException.Code.NOT_FOUND) return VisaApiError.NotPaeraCustomer
if (value.code == ApiResponseError.HttpException.Code.UNAUTHORIZED) return VisaApiError.RefreshTokenExpired

View file

@ -281,8 +281,10 @@ internal class DefaultWalletManagersFacade @Inject constructor(
is Result.Success -> PaginationWrapper(
currentPage = sdkPageConverter.convert(page),
nextPage = sdkPageConverter.convert(itemsResult.data.nextPage),
items = SdkTransactionHistoryItemConverter(smartContractMethods = readSmartContractMethods())
.convertList(itemsResult.data.items),
items = SdkTransactionHistoryItemConverter(
smartContractMethods = readSmartContractMethods(),
yieldSupplyAddresses = YIELD_SUPPLY_ADDRESSES,
).convertList(itemsResult.data.items),
)
is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage)
}

View file

@ -8,9 +8,13 @@ import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem as
internal class SdkTransactionHistoryItemConverter(
smartContractMethods: Map<String, SmartContractMethod>,
yieldSupplyAddresses: Set<String>,
) : Converter<SdkTransactionHistoryItem, TxInfo> {
private val typeConverter by lazy { SdkTransactionTypeConverter(smartContractMethods) }
private val typeConverter by lazy { SdkTransactionTypeConverter(
smartContractMethods = smartContractMethods,
yieldSupplyAddresses = yieldSupplyAddresses,
) }
override fun convert(value: SdkTransactionHistoryItem): TxInfo = TxInfo(
txHash = value.txHash,
@ -24,7 +28,7 @@ internal class SdkTransactionHistoryItemConverter(
SdkTransactionHistoryItem.TransactionStatus.Failed -> TxInfo.TransactionStatus.Failed
SdkTransactionHistoryItem.TransactionStatus.Unconfirmed -> TxInfo.TransactionStatus.Unconfirmed
},
type = typeConverter.convert(value.type to value.destinationType.toDomain()),
type = typeConverter.convert(value),
amount = requireNotNull(value.amount.value) { "Transaction amount value must not be null" },
)

View file

@ -1,5 +1,6 @@
package com.tangem.data.walletmanager.utils
import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem
import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem.TransactionType
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyEnterCallData
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplyExitCallData
@ -11,17 +12,29 @@ import com.tangem.utils.converter.Converter
internal class SdkTransactionTypeConverter(
private val smartContractMethods: Map<String, SmartContractMethod>,
) : Converter<Pair<TransactionType, TxInfo.DestinationType>, TxInfo.TransactionType> {
private val yieldSupplyAddresses: Set<String>,
) : Converter<TransactionHistoryItem, TxInfo.TransactionType> {
override fun convert(value: Pair<TransactionType, TxInfo.DestinationType>): TxInfo.TransactionType {
val (type, destination) = value
override fun convert(value: TransactionHistoryItem): TxInfo.TransactionType {
val (type, destination) = value.type to value.destinationType
val source = value.sourceType
return when (type) {
is TransactionType.ContractMethod -> {
getTransactionType(methodName = smartContractMethods[type.id]?.name, type.callData, destination)
getTransactionType(
methodName = smartContractMethods[type.id]?.name,
callData = type.callData,
destination = destination,
source = source,
)
}
is TransactionType.ContractMethodName -> {
getTransactionType(methodName = type.name, type.callData, destination)
getTransactionType(
methodName = type.name,
callData = type.callData,
destination = destination,
source = source,
)
}
is TransactionType.Transfer -> {
TxInfo.TransactionType.Transfer
@ -48,7 +61,8 @@ internal class SdkTransactionTypeConverter(
private fun getTransactionType(
methodName: String?,
callData: String?,
destination: TxInfo.DestinationType,
destination: TransactionHistoryItem.DestinationType,
source: TransactionHistoryItem.SourceType,
): TxInfo.TransactionType {
return when (methodName) {
"transfer" -> TxInfo.TransactionType.Transfer
@ -70,7 +84,21 @@ internal class SdkTransactionTypeConverter(
"withdrawRewardsPOL",
-> TxInfo.TransactionType.Staking.ClaimRewards
"redelegate" -> TxInfo.TransactionType.Staking.Restake
"yieldSend" -> TxInfo.TransactionType.YieldSupply.Send
"yieldSend" -> {
val sourceAddresses = when (source) {
is TransactionHistoryItem.SourceType.Multiple -> source.addresses
is TransactionHistoryItem.SourceType.Single -> listOf(source.address)
}.map {
it.lowercase()
}.toSet()
val isYieldSupplyWithdraw =
yieldSupplyAddresses.intersect(sourceAddresses).isNotEmpty()
TxInfo.TransactionType.YieldSupply.Send(
isYieldSupplyWithdraw = isYieldSupplyWithdraw,
)
}
"enterProtocolByOwner" -> callData?.let { data ->
TxInfo.TransactionType.YieldSupply.Enter(
EthereumYieldSupplyEnterCallData.decode(data)?.tokenContractAddress.orEmpty(),
@ -82,7 +110,7 @@ internal class SdkTransactionTypeConverter(
)
}
"deployYieldModule" -> TxInfo.TransactionType.YieldSupply.DeployContract(
(destination as? TxInfo.DestinationType.Single)?.addressType?.address.orEmpty(),
(destination as? TransactionHistoryItem.DestinationType.Single)?.addressType?.address.orEmpty(),
)
"initYieldToken" -> callData?.let { data ->
TxInfo.TransactionType.YieldSupply.InitializeToken(

View file

@ -0,0 +1,113 @@
package com.tangem.data.walletmanager.utils
// Remove after moving this data to BE
internal val YIELD_SUPPLY_ADDRESSES: Set<String> = setOf(
"0x00901a076785e0906d1028c7d6372d247bec7d61",
"0x00907f9921424583e7ffbfedf84f92b7b2be4977",
"0x018008bfb33d285247a21d44e50697654f754e63",
"0x067ae75628177fd257c2b1e500993e1a0babcbd1",
"0x078f358208685046a11c85e8ad32895ded33a249",
"0x0a1d576f3efef75b330424287a95a366e8281d54",
"0x0b925ed163218f6662a35e0f0371ac234f9e9371",
"0x0c0d01abf3e6adfca0989ebba9d6e85dd58eab1e",
"0x10ac93971cdb1f5c778144084242374473c350da",
"0x191c10aa4af7c30e871e70c95db0e4eb77237530",
"0x1ba9843bd4327c6c77011406de5fa8749f7e3479",
"0x1c0e06a0b1a4c160c17545ff2a951bfca57c0002",
"0x23878914efe38d27c4d67ab83ed1b93a74d4086a",
"0x24ab03a9a5bc2c49e5523e8d915a3536ac38b91d",
"0x2516e7b3f76294e03c42aa4c5b5b4dce9c436fb8",
"0x285866acb0d60105b4ed350a463361c2d9afa0e2",
"0x2d62109243b87c4ba3ee7ba1d91b0dd0a074d7b1",
"0x2e94171493fabe316b6205f1585779c887771e2f",
"0x2edff5af94334fbd7c38ae318edf1c40e072b73b",
"0x312ffc57778cefa11989733e6e08143e7e229c1c",
"0x32a6268f9ba3642dda7892add74f1d34469a4259",
"0x38a5357ce55c81add62abc84fb32981e2626adef",
"0x38c503a438185cde29b5cf4dc1442fd6f074f1cc",
"0x38d693ce1df5aadf7bc62595a37d667ad57922e5",
"0x3fe6a295459fae07df8a0cecc36f37160fe86aa9",
"0x40b4baecc69b882e8804f9286b12228c27f8c9bf",
"0x4199cc1f5ed0d796563d7ccb2e036253e2c18281",
"0x44705f578135cc5d703b4c9c122528c73eb87145",
"0x4579a27af00a62c0eb156349f31b345c08386419",
"0x481a2acf3a72ffdc602a9541896ca1db87f86cf7",
"0x4b0821e768ed9039a70ed1e80e15e76a5be5df5f",
"0x4c612e3b15b96ff9a6faed838f8d07d479a8dd4c",
"0x4d5f47fa6a74757f35c14fd3a6ef8e3c9bc514e8",
"0x4e2a4d9b3df7aae73b418bd39f3af9e148e3f479",
"0x4e65fe4dba92790696d040ac24aa414708f5c0ab",
"0x4f5923fc5fd4a93352581b38b7cd26943012decf",
"0x513c7e3a9c69ca3e22550ef58ac1c0088e918fff",
"0x545bd6c032efdde65a377a6719def2796c8e0f2e",
"0x56a7ddc4e848ebf43845854205ad71d5d5f72d3d",
"0x5b502e3796385e1e9755d7043b9c945c3accec9c",
"0x5c647ce0ae10658ec44fa4e11a51c96e94efd1dd",
"0x5e8c8a7243651db1384c0ddfdbe39761e8e7e51a",
"0x5ee5bf7ae06d1be5997a1a72006fe6c607ec6de8",
"0x5f4a0873a3a02f7c0cb0e13a1d4362a1ad90e751",
"0x5f9190496e0dfc831c3bd307978de4a245e2f5cd",
"0x5fefd7069a7d91d01f269dade14526ccf3487810",
"0x625e7708f30ca75bfd92586e17077590c60eb4cd",
"0x62fc96b27a510cf4977b59ff952dc32378cc221d",
"0x6533afac2e7bccb20dca161449a13a32d391fb00",
"0x65906988adee75306021c417a1a3458040239602",
"0x67eaf2bee4384a2f84da9eb8105c661c123736ba",
"0x6ab707aca953edaefbc4fd23ba73294241490620",
"0x6b030ff3fb9956b1b69f475b77ae0d3cf2cc5afa",
"0x6d80113e533a2c0fe82eabd35f1875dcea89ea97",
"0x71aef7b30728b9bb371578f36c5a1f1502a5723e",
"0x724dc807b04555b71ed48a6896b6f41593b8c637",
"0x75bd1a659bdc62e4c313950d44a2416fab43e785",
"0x7b95ec873268a6bfc6427e7a28e396db9d0ebc65",
"0x7c307e128efa31f540f2e2d976c995e0b65f51f6",
"0x80a94c36747cf51b2fbabdff045f6d22c1930ed1",
"0x80ca0d8c38d2e2bcbab66aa1648bd1c7160500fe",
"0x82e64f49ed5ec1bc6e43dad4fc8af9bb3a2312ee",
"0x82f9c5ad306bba1ad0de49bb5fa6f01bf61085ef",
"0x8437d7c167dfb82ed4cb79cd44b7a32a1dd95c77",
"0x8a2b6f94ff3a89a03e8c02ee92b55af90c9454a2",
"0x8a458a9dc9048e005d22849f470891b840296619",
"0x8a9fde6925a839f6b1932d16b36ac026f8d3fbdb",
"0x8eb270e296023e9d92081fdf967ddd7878724424",
"0x8ffdf2de812095b1d19cb146e4c004587c0a0692",
"0x90072a4aa69b5eb74984ab823efc5f91e90b3a72",
"0x90da57e0a6c0d166bf15764e03b83745dc90025b",
"0x927709711794f3de5ddbf1d176bee2d55ba13c21",
"0x977b6fc5de62598b08c85ac8cf2b745874e8b78c",
"0x98c23e9d8f34fefb1b7bd6a91b7ff122f4e16f5c",
"0x99cbc45ea5bb7ef3a5bc08fb1b7e56bb2442ef0d",
"0x9a44fd41566876a39655f74971a3a6ea0a17a454",
"0x9b00a09492a626678e5a3009982191586c444df9",
"0xa4d94019934d8333ef880abffbf2fdd611c762bd",
"0xa700b4eb416be35b2911fd5dee80678ff64ff6c9",
"0xa9251ca9de909cb71783723713b21e4233fbf1b1",
"0xaa0200d169ff3ba9385c12e073c5d1d30434ae7b",
"0xaa6e91c82942aeae040303bf96c15a6dbcb82ca0",
"0xb76cf92076adbf1d9c39294fa8e7a67579fde357",
"0xb82fa9f31612989525992fcfbb09ab22eff5c85a",
"0xbcffb4b3beadc989bd1458740952af6ec8fbe431",
"0xbdb9300b7cde636d9cd4aff00f6f009ffbbc8ee6",
"0xbdfa7b7893081b35fb54027489e2bc7a38275129",
"0xbdfd4e51d3c14a232135f04988a42576efb31519",
"0xbe54767735fb7acca2aa7e2d209a6f705073536d",
"0xc45a479877e1e9dfe9fcd4056c699575a1045daa",
"0xc7b4c17861357b8abb91f25581e7263e08dcb59c",
"0xcc9ee9483f662091a1de4795249e24ac0ac2630f",
"0xcca43cef272c30415866914351fdfc3e881bb7c2",
"0xcf3d55c10db69f28fd1a75bd73f3d8a2d9c595ad",
"0xd4a0e0b9149bcee3c920d2e00b5de09138fd8bb7",
"0xd4e245848d6e1220dbe62e155d89fa327e43cb06",
"0xdd5745756c2de109183c6b5bb886f9207bef114d",
"0xde6ef6cb4abd3a473ffc2942eef5d84536f8e864",
"0xe50fa9b3c56ffb159cb0fca61f5c9d750e8128c8",
"0xe728577e9a1fe7032bc309b4541f58f45443866e",
"0xea1132120ddcdda2f119e99fa7a27a0d036f7ac9",
"0xebe517846d0f36eced99c735cbf6131e1feb775d",
"0xec4ef66d4fceeba34abb4de69db391bc5476ccc8",
"0xf329e36c7bf6e5e86ce2150875a84ce77f477375",
"0xf59036caebea7dc4b86638dfa2e3c97da9fccd40",
"0xf611aeb5013fd2c0511c9cd55c7dc5c1140741a6",
"0xf6d2224916ddfbbab6e6bd0d1b7034f4ae0cab18",
"0xfa82580c16a31d0c1bc632a36f82e83efef3eec0",
)

View file

@ -7,16 +7,16 @@ fun CryptoCurrency.Token.yieldSupplyKey(): String {
}
fun CryptoCurrencyStatus.hasNotSuppliedAmount(): Boolean {
val notSupplied = notSuppliedAmountOrNull() ?: return false
val notSupplied = notSuppliedCryptoAmountOrNull() ?: return false
return notSupplied > BigDecimal.ZERO
}
fun CryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustAmount: BigDecimal): Boolean {
val notSupplied = notSuppliedAmountOrNull() ?: return false
fun CryptoCurrencyStatus.shouldShowNotSuppliedNotification(dustAmount: BigDecimal): Boolean {
val notSupplied = notSuppliedCryptoAmountOrNull()?.multiply(this.value.fiatRate) ?: return false
return notSupplied >= dustAmount
}
fun CryptoCurrencyStatus.notSuppliedAmountOrNull(): BigDecimal? {
fun CryptoCurrencyStatus.notSuppliedCryptoAmountOrNull(): BigDecimal? {
if (this.currency !is CryptoCurrency.Token) return null
val supplyStatus = this.value.yieldSupplyStatus

View file

@ -124,9 +124,10 @@ data class TxInfo(
}
@Serializable
data object Send : YieldSupply {
override val address: String? = null
}
data class Send(
override val address: String? = null,
val isYieldSupplyWithdraw: Boolean,
) : YieldSupply
@Serializable
data class DeployContract(override val address: String) : YieldSupply

View file

@ -0,0 +1,417 @@
package com.tangem.domain.models.currency
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.math.BigDecimal
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CryptoCurrencyExtensionsTest {
@Test
fun `GIVEN currency is Coin WHEN hasNotSuppliedAmount THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Coin>(),
)
val result = status.hasNotSuppliedAmount()
assertThat(result).isFalse()
}
@Test
fun `GIVEN yieldSupplyStatus is null WHEN hasNotSuppliedAmount THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = null,
)
val result = status.hasNotSuppliedAmount()
assertThat(result).isFalse()
}
@Test
fun `GIVEN yieldSupplyStatus isActive is false WHEN hasNotSuppliedAmount THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = false,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal.TEN,
),
)
val result = status.hasNotSuppliedAmount()
assertThat(result).isFalse()
}
@Test
fun `GIVEN effectiveProtocolBalance is null WHEN hasNotSuppliedAmount THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = null,
),
amount = BigDecimal.TEN,
)
val result = status.hasNotSuppliedAmount()
assertThat(result).isFalse()
}
@Test
fun `GIVEN amount is null WHEN hasNotSuppliedAmount THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal.TEN,
),
amount = null,
)
val result = status.hasNotSuppliedAmount()
assertThat(result).isFalse()
}
@Test
fun `GIVEN notSupplied is zero WHEN hasNotSuppliedAmount THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal.TEN,
),
amount = BigDecimal.TEN,
)
val result = status.hasNotSuppliedAmount()
assertThat(result).isFalse()
}
@Test
fun `GIVEN notSupplied is negative WHEN hasNotSuppliedAmount THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal.TEN,
),
amount = BigDecimal.ONE,
)
val result = status.hasNotSuppliedAmount()
assertThat(result).isFalse()
}
@Test
fun `GIVEN notSupplied is positive WHEN hasNotSuppliedAmount THEN returns true`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal.ONE,
),
amount = BigDecimal.TEN,
)
val result = status.hasNotSuppliedAmount()
assertThat(result).isTrue()
}
@Test
fun `GIVEN currency is Coin WHEN shouldShowNotSuppliedNotification THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Coin>(),
)
val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.ONE)
assertThat(result).isFalse()
}
@Test
fun `GIVEN yieldSupplyStatus is null WHEN shouldShowNotSuppliedNotification THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = null,
)
val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.ONE)
assertThat(result).isFalse()
}
@Test
fun `GIVEN yieldSupplyStatus isActive is false WHEN shouldShowNotSuppliedNotification THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = false,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal.TEN,
),
)
val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.ONE)
assertThat(result).isFalse()
}
@Test
fun `GIVEN effectiveProtocolBalance is null WHEN shouldShowNotSuppliedNotification THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = null,
),
amount = BigDecimal.TEN,
fiatRate = BigDecimal.ONE,
)
val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.ONE)
assertThat(result).isFalse()
}
@Test
fun `GIVEN notSupplied in fiat is less than dustAmount WHEN shouldShowNotSuppliedNotification THEN returns false`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal("9"),
),
amount = BigDecimal.TEN,
fiatRate = BigDecimal.ONE,
)
val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.TEN)
assertThat(result).isFalse()
}
@Test
fun `GIVEN notSupplied in fiat equals dustAmount WHEN shouldShowNotSuppliedNotification THEN returns true`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal("5"),
),
amount = BigDecimal.TEN,
fiatRate = BigDecimal("2"),
)
val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.TEN)
assertThat(result).isTrue()
}
@Test
fun `GIVEN notSupplied in fiat is greater than dustAmount WHEN shouldShowNotSuppliedNotification THEN returns true`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal.ONE,
),
amount = BigDecimal.TEN,
fiatRate = BigDecimal("2"),
)
val result = status.shouldShowNotSuppliedNotification(dustAmount = BigDecimal.TEN)
assertThat(result).isTrue()
}
@Test
fun `GIVEN USDT with EUR fiat rate and notSupplied below dust WHEN shouldShowNotSuppliedNotification THEN returns false`() {
val usdtToEurRate = BigDecimal("0.93")
val notSuppliedUsdt = BigDecimal("0.05")
val protocolBalance = BigDecimal("100")
val totalAmount = protocolBalance.add(notSuppliedUsdt)
val dustAmountEur = BigDecimal("0.1")
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = protocolBalance,
),
amount = totalAmount,
fiatRate = usdtToEurRate,
)
val result = status.shouldShowNotSuppliedNotification(dustAmount = dustAmountEur)
assertThat(result).isFalse()
}
@Test
fun `GIVEN USDT with EUR fiat rate and notSupplied above dust WHEN shouldShowNotSuppliedNotification THEN returns true`() {
val usdtToEurRate = BigDecimal("0.93")
val notSuppliedUsdt = BigDecimal("0.15")
val protocolBalance = BigDecimal("100")
val totalAmount = protocolBalance.add(notSuppliedUsdt)
val dustAmountEur = BigDecimal("0.1")
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = protocolBalance,
),
amount = totalAmount,
fiatRate = usdtToEurRate,
)
val result = status.shouldShowNotSuppliedNotification(dustAmount = dustAmountEur)
assertThat(result).isTrue()
}
@Test
fun `GIVEN currency is Coin WHEN notSuppliedCryptoAmountOrNull THEN returns null`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Coin>(),
)
val result = status.notSuppliedCryptoAmountOrNull()
assertThat(result).isNull()
}
@Test
fun `GIVEN yieldSupplyStatus is null WHEN notSuppliedCryptoAmountOrNull THEN returns null`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = null,
)
val result = status.notSuppliedCryptoAmountOrNull()
assertThat(result).isNull()
}
@Test
fun `GIVEN yieldSupplyStatus isActive is false WHEN notSuppliedCryptoAmountOrNull THEN returns null`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = false,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal.TEN,
),
)
val result = status.notSuppliedCryptoAmountOrNull()
assertThat(result).isNull()
}
@Test
fun `GIVEN effectiveProtocolBalance is null WHEN notSuppliedCryptoAmountOrNull THEN returns null`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = null,
),
amount = BigDecimal.TEN,
)
val result = status.notSuppliedCryptoAmountOrNull()
assertThat(result).isNull()
}
@Test
fun `GIVEN amount is null WHEN notSuppliedCryptoAmountOrNull THEN returns null`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal.TEN,
),
amount = null,
)
val result = status.notSuppliedCryptoAmountOrNull()
assertThat(result).isNull()
}
@Test
fun `GIVEN valid data WHEN notSuppliedCryptoAmountOrNull THEN returns amount minus protocolBalance`() {
val status = createCryptoCurrencyStatus(
currency = mockk<CryptoCurrency.Token>(),
yieldSupplyStatus = YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = BigDecimal("3"),
),
amount = BigDecimal.TEN,
)
val result = status.notSuppliedCryptoAmountOrNull()
assertThat(result).isEqualTo(BigDecimal("7"))
}
private fun createCryptoCurrencyStatus(
currency: CryptoCurrency,
yieldSupplyStatus: YieldSupplyStatus? = null,
amount: BigDecimal? = null,
fiatRate: BigDecimal? = null,
): CryptoCurrencyStatus {
val value = mockk<CryptoCurrencyStatus.Value> {
every { this@mockk.yieldSupplyStatus } returns yieldSupplyStatus
every { this@mockk.amount } returns amount
every { this@mockk.fiatRate } returns fiatRate
}
return CryptoCurrencyStatus(currency = currency, value = value)
}
}

View file

@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class TangemPayDetailsConfig(
val cardId: String,
val isPinSet: Boolean,
val cardFrozenState: TangemPayCardFrozenState,
val customerWalletAddress: String,
val cardNumberEnd: String,

View file

@ -65,6 +65,7 @@ sealed class VisaApiError(
data object WithdrawalDataError : VisaApiError(104004003)
data object SignWithdrawError : VisaApiError(104004004)
data object WithdrawError : VisaApiError(104004005)
data object ServerUnavailable : VisaApiError(104004006)
companion object {
fun fromBackendError(backendErrorCode: Int): VisaApiError {

View file

@ -4,5 +4,5 @@ import com.tangem.domain.models.wallet.UserWallet
interface TangemPayEligibilityManager {
suspend fun getEligibleWallets(): List<UserWallet>
suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List<UserWallet>
}

View file

@ -32,5 +32,6 @@ data class CustomerInfo(
val currencyCode: String,
val customerWalletAddress: String,
val depositAddress: String?,
val isPinSet: Boolean,
)
}

View file

@ -15,6 +15,8 @@ interface TangemPayCardDetailsRepository {
suspend fun revealCardDetails(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardDetails>
suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either<UniversalError, String?>
suspend fun setPin(userWalletId: UserWalletId, pin: String): Either<UniversalError, SetPinResult>
suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either<UniversalError, Boolean>

View file

@ -46,7 +46,11 @@ class TangemPayMainScreenCustomerInfoUseCase(
.fold(
ifLeft = { error ->
Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}")
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
if (error is VisaApiError.NotPaeraCustomer) {
showOnboardingBannerIfEligible(userWalletId)
} else {
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
}
},
ifRight = { hasTangemPay ->
Timber.tag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay")
@ -61,21 +65,27 @@ class TangemPayMainScreenCustomerInfoUseCase(
updateState(userWalletId, result)
} else {
// if there's no tangem pay, check eligibility and show onboarding banner
val isEligible = eligibilityManager.getEligibleWallets().any { it.walletId == userWalletId }
if (isEligible) {
if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) {
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
} else {
updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right())
}
} else {
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
}
showOnboardingBannerIfEligible(userWalletId)
}
},
)
}
private suspend fun showOnboardingBannerIfEligible(userWalletId: UserWalletId) {
val isEligible = eligibilityManager
.getEligibleWallets(shouldExcludePaeraCustomers = false)
.any { it.walletId == userWalletId }
if (isEligible) {
if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) {
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
} else {
updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right())
}
} else {
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
}
}
operator fun invoke(
userWalletId: UserWalletId,
): Flow<Either<TangemPayCustomerInfoError, MainCustomerInfoContentState>> {

View file

@ -35,6 +35,8 @@ dependencies {
implementation(projects.domain.hotWallet)
// endregion
implementation(projects.common)
// region Tangem libraries
implementation(tangemDeps.blockchain) // android-library
implementation(tangemDeps.card.core)

View file

@ -1,23 +1,24 @@
package com.tangem.domain.wallets.usecase
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import com.tangem.common.TangemSiteUrlBuilder
import java.util.Locale
class GenerateBuyTangemCardLinkUseCase {
suspend operator fun invoke(): String = suspendCoroutine { cont ->
Firebase.analytics.appInstanceId
.addOnSuccessListener { id ->
cont.resume("$NEW_BUY_WALLET_URL&app_instance_id=$id")
}
.addOnFailureListener {
cont.resume(NEW_BUY_WALLET_URL)
}
suspend operator fun invoke(source: Source): String {
return invoke(source.utmCampaign)
}
companion object {
private const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app"
suspend operator fun invoke(utmCampaign: String?): String {
val langCode = Locale.getDefault().language
val utmTags = TangemSiteUrlBuilder.getUtmTags(utmCampaign)
return "https://buy.tangem.com/$langCode?$utmTags"
}
enum class Source(val utmCampaign: String) {
Creation("prospect"),
Settings("users"),
Backup("backup"),
Upgrade("upgrade"),
}
}

View file

@ -1,15 +1,27 @@
package com.tangem.domain.yield.supply.usecase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import java.math.BigDecimal
/**
* Use case for getting dust minimum amount for yield supply.
*
* Returns the minimum amount based on the selected [AppCurrency].
* If [AppCurrency] is in [SUPPORTED_DUST_CURRENCIES], returns [DUST_MIN_AMOUNT],
* otherwise returns the original [minAmount] with trailing zeros stripped.
*/
class YieldSupplyGetDustMinAmountUseCase {
operator fun invoke(minAmount: BigDecimal, appCurrency: AppCurrency): BigDecimal {
operator fun invoke(
minAmountTokenCurrency: BigDecimal,
appCurrency: AppCurrency,
tokenCryptoCurrencyStatus: CryptoCurrencyStatus,
): BigDecimal {
return if (appCurrency.code in SUPPORTED_DUST_CURRENCIES) {
DUST_MIN_AMOUNT
} else {
minAmount.stripTrailingZeros()
minAmountTokenCurrency.multiply(tokenCryptoCurrencyStatus.value.fiatRate)
}
}

View file

@ -13,6 +13,14 @@ import com.tangem.domain.yield.supply.fixFee
import java.math.BigDecimal
import java.math.RoundingMode
/**
* Use case that calculates the minimum amount required for yield supply operations.
*
* The calculation is based on the estimated transaction fee converted to the token currency,
* with a buffer multiplier applied to account for fee fluctuations.
*
* @return [BigDecimal] minimum amount in token currency (not native/network currency)
*/
class YieldSupplyMinAmountUseCase(
private val feeRepository: FeeRepository,
private val quotesRepository: QuotesRepository,

View file

@ -2,6 +2,10 @@ package com.tangem.domain.yield.supply.usecase
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import org.junit.jupiter.api.Test
import java.math.BigDecimal
@ -13,19 +17,79 @@ class YieldSupplyGetDustMinAmountUseCaseTest {
fun `GIVEN supported currency WHEN invoke THEN return dust min amount`() {
val minAmount = BigDecimal("123.456")
val appCurrency = AppCurrency(code = "EUR", name = "Euro", symbol = "")
val tokenStatus = createTokenStatus(fiatRate = BigDecimal("2.0"))
val result = useCase(minAmount, appCurrency)
val result = useCase(minAmount, appCurrency, tokenStatus)
assertThat(result).isEqualTo(BigDecimal("0.1"))
}
@Test
fun `GIVEN unsupported currency WHEN invoke THEN return min amount stripped`() {
val minAmount = BigDecimal("1.2300")
fun `GIVEN unsupported currency WHEN invoke THEN return min amount multiplied by fiat rate`() {
val minAmount = BigDecimal("1.23")
val fiatRate = BigDecimal("150.0")
val appCurrency = AppCurrency(code = "JPY", name = "Japanese Yen", symbol = "¥")
val tokenStatus = createTokenStatus(fiatRate = fiatRate)
val result = useCase(minAmount, appCurrency)
val result = useCase(minAmount, appCurrency, tokenStatus)
assertThat(result).isEqualTo(BigDecimal("1.23"))
assertThat(result).isEqualTo(minAmount.multiply(fiatRate))
}
private fun createTokenStatus(fiatRate: BigDecimal): CryptoCurrencyStatus {
val network = createNetwork()
val token = createToken(network)
return CryptoCurrencyStatus(
currency = token,
value = CryptoCurrencyStatus.Loaded(
amount = BigDecimal.ZERO,
fiatAmount = BigDecimal.ZERO,
fiatRate = fiatRate,
priceChange = BigDecimal.ZERO,
stakingBalance = null,
yieldSupplyStatus = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = NetworkAddress.Single(
NetworkAddress.Address(value = "0xabc", type = NetworkAddress.Address.Type.Primary),
),
sources = CryptoCurrencyStatus.Sources(),
),
)
}
private fun createNetwork(): Network {
val derivationPath = Network.DerivationPath.None
return Network(
id = Network.ID(Network.RawID("polygon"), derivationPath),
backendId = "polygon",
name = "Polygon",
currencySymbol = "MATIC",
derivationPath = derivationPath,
isTestnet = false,
standardType = Network.StandardType.ERC20,
hasFiatFeeRate = false,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.ENS,
)
}
private fun createToken(network: Network): CryptoCurrency.Token {
val tokenId = CryptoCurrency.ID(
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
body = CryptoCurrency.ID.Body.NetworkId(network.rawId),
suffix = CryptoCurrency.ID.Suffix.RawID("test-token", "0xContract"),
)
return CryptoCurrency.Token(
id = tokenId,
network = network,
name = "Test Token",
symbol = "TT",
decimals = 18,
iconUrl = null,
isCustom = false,
contractAddress = "0xContract",
)
}
}

View file

@ -39,6 +39,7 @@ dependencies {
/** Common */
implementation(projects.common.ui)
implementation(projects.common.routing)
implementation(projects.common)
/** Tangem libraries */
implementation(projects.libs.tangemSdkApi)

View file

@ -1,5 +1,6 @@
package com.tangem.features.createwalletselection
import com.tangem.common.TangemBlogUrlBuilder
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
@ -84,6 +85,7 @@ internal class CreateWalletSelectionModel @Inject constructor(
),
onBuyClick = ::onBuyClick,
shouldShowAlreadyHaveWallet = true,
onWhatToChooseClick = ::onWhatToChooseClick,
),
)
@ -124,7 +126,14 @@ internal class CreateWalletSelectionModel @Inject constructor(
private fun onBuyClick() {
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.AddNewWallet))
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
generateBuyTangemCardLinkUseCase
.invoke(GenerateBuyTangemCardLinkUseCase.Source.Creation).let { urlOpener.openUrl(it) }
}
}
private fun onWhatToChooseClick() {
modelScope.launch {
urlOpener.openUrl(TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.WhatWalletToChoose))
}
}

View file

@ -10,6 +10,7 @@ internal data class CreateWalletSelectionUM(
val blocks: ImmutableList<Block>,
val onBackClick: () -> Unit,
val onBuyClick: () -> Unit,
val onWhatToChooseClick: () -> Unit,
) {
data class Block(
val title: TextReference,

View file

@ -19,7 +19,9 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.TextButton
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
@ -61,14 +63,14 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
},
title = { },
actions = {
Text(
modifier = Modifier
.padding(16.dp),
TextButton(
modifier = Modifier.clip(TangemTheme.shapes.roundedCornersLarge),
text = stringResourceSafe(R.string.wallet_add_support_title),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textStyle = TangemTheme.typography.body1,
colors = TangemButtonsDefaults.defaultTextButtonColors.copy(
contentColor = TangemTheme.colors.text.primary1,
),
onClick = state.onWhatToChooseClick,
)
},
)
@ -288,6 +290,7 @@ private fun PreviewCreateWalletContent() {
),
shouldShowAlreadyHaveWallet = true,
onBuyClick = { },
onWhatToChooseClick = { },
),
)
}

View file

@ -162,7 +162,8 @@ internal class CreateWalletStartModel @Inject constructor(
private fun onBuyClick() {
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateWalletIntro))
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
generateBuyTangemCardLinkUseCase
.invoke(GenerateBuyTangemCardLinkUseCase.Source.Creation).let { urlOpener.openUrl(it) }
}
}

View file

@ -232,8 +232,10 @@ internal class DetailsModel @Inject constructor(
modelScope.launch {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Settings))
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
generateBuyTangemCardLinkUseCase
.invoke(GenerateBuyTangemCardLinkUseCase.Source.Settings).let { urlOpener.openUrl(it) }
} else {
// This is incorrect implementation of buy link generation, but it is left here
urlOpener.openUrl(buildBuyLink())
}
}
@ -248,7 +250,9 @@ internal class DetailsModel @Inject constructor(
private fun addTangemPayItemIfEligible() {
if (!tangemPayFeatureToggles.isTangemPayEnabled) return
modelScope.launch {
val isEligible = tangemPayEligibilityManager.getEligibleWallets().isNotEmpty()
val isEligible = tangemPayEligibilityManager
.getEligibleWallets(shouldExcludePaeraCustomers = true)
.isNotEmpty()
if (isEligible) {
items.update { itemsBuilder.addVisaItem(it) }
}

View file

@ -135,7 +135,7 @@ internal class HomeModel @Inject constructor(
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards())
analyticsEventHandler.send(Shop.ScreenOpened())
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
generateBuyTangemCardLinkUseCase.invoke(null).let { urlOpener.openUrl(it) }
}
}

View file

@ -53,6 +53,7 @@ internal class AccessCodeComponent @AssistedInject constructor(
interface ModelCallbacks {
fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String)
fun onAccessCodeUpdateStarted(userWalletId: UserWalletId)
fun onAccessCodeUpdated(userWalletId: UserWalletId)
}

View file

@ -16,6 +16,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireHotWallet
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.SetAskBiometryShownUseCase
import com.tangem.domain.settings.ShouldShowAskBiometryUseCase
@ -31,6 +32,7 @@ import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -80,6 +82,7 @@ internal class AccessCodeModel @Inject constructor(
accessCode = "",
accessCodeColor = PinTextColor.Primary,
onAccessCodeChange = ::onAccessCodeChange,
isLoading = false,
isConfirmMode = params.accessCodeToConfirm != null,
)
@ -117,6 +120,18 @@ internal class AccessCodeModel @Inject constructor(
}
}
private suspend fun setLoadingIfLongJob(job: Job) {
delay(timeMillis = 2000)
if (job.isActive) {
uiState.update { currentState ->
currentState.copy(
isLoading = true,
)
}
}
}
private fun onNewCodeSet() {
modelScope.launch {
delay(timeMillis = SUCCESS_DISPLAY_DURATION_MS)
@ -187,56 +202,65 @@ internal class AccessCodeModel @Inject constructor(
}
private fun setCode(userWalletId: UserWalletId, accessCode: String) {
params.callbacks.onAccessCodeUpdateStarted(params.userWalletId)
modelScope.launch {
val userWallet = getUserWalletUseCase(userWalletId)
.getOrElse { error("User wallet with id $userWalletId not found") }
if (userWallet !is UserWallet.Hot) return@launch
.requireHotWallet()
tryToAskForBiometry()
userWalletsListRepository.setLock(
userWallet.walletId,
UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()),
)
if (walletsRepository.useBiometricAuthentication()) {
userWalletsListRepository.setLock(
userWallet.walletId,
UserWalletsListRepository.LockMethod.Biometric,
)
val settingCodeJob = launch(dispatchers.main) {
setCodeOperation(userWallet, accessCode)
params.callbacks.onAccessCodeUpdated(params.userWalletId)
}
val unlockHotWallet = getHotWalletContextualUnlockUseCase(userWallet.hotWalletId)
.getOrNull()
?: run {
require(userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword) {
"Something went wrong. Hot wallet is locked and cannot be unlocked with NoAuth"
}
setLoadingIfLongJob(settingCodeJob)
}
}
hotWalletAccessor.unlockContextual(userWallet.hotWalletId)
private suspend fun setCodeOperation(userWallet: UserWallet.Hot, accessCode: String) {
userWalletsListRepository.setLock(
userWalletId = userWallet.walletId,
lockMethod = UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()),
)
if (walletsRepository.useBiometricAuthentication()) {
userWalletsListRepository.setLock(
userWalletId = userWallet.walletId,
lockMethod = UserWalletsListRepository.LockMethod.Biometric,
)
}
val unlockHotWallet = getHotWalletContextualUnlockUseCase(userWallet.hotWalletId)
.getOrNull()
?: run {
require(userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword) {
"Something went wrong. Hot wallet is locked and cannot be unlocked with NoAuth"
}
var updatedHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = unlockHotWallet,
auth = HotAuth.Password(accessCode.toCharArray()),
)
if (walletsRepository.requireAccessCode().not()) {
updatedHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = unlockHotWallet,
auth = HotAuth.Biometry,
)
hotWalletAccessor.unlockContextual(userWallet.hotWalletId)
}
userWalletsListRepository.saveWithoutLock(
userWallet.copy(hotWalletId = updatedHotWalletId),
canOverride = true,
var updatedHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = unlockHotWallet,
auth = HotAuth.Password(accessCode.toCharArray()),
)
if (walletsRepository.requireAccessCode().not()) {
updatedHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = unlockHotWallet,
auth = HotAuth.Biometry,
)
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
params.callbacks.onAccessCodeUpdated(params.userWalletId)
}
userWalletsListRepository.saveWithoutLock(
userWallet.copy(hotWalletId = updatedHotWalletId),
canOverride = true,
)
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
}
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -8,6 +8,7 @@ import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH
internal data class AccessCodeUM(
val accessCode: String,
val accessCodeColor: PinTextColor,
val isLoading: Boolean,
val onAccessCodeChange: (String) -> Unit,
val isConfirmMode: Boolean,
val requestFocus: StateEvent<Unit> = consumedEvent(),

View file

@ -3,8 +3,10 @@ package com.tangem.features.hotwallet.accesscode.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -16,9 +18,14 @@ import com.tangem.core.res.R
import com.tangem.core.ui.components.fields.PinTextColor
import com.tangem.core.ui.components.fields.PinTextField
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Suppress("LongParameterList", "LongMethod")
@Composable
@ -27,6 +34,24 @@ internal fun AccessCode(
modifier: Modifier = Modifier,
focusRequester: FocusRequester = remember { FocusRequester() },
) {
if (state.isLoading) {
Box(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary)
.navigationBarsPadding(),
) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.size(20.dp),
color = TangemTheme.colors.icon.secondary,
strokeWidth = 2.dp,
)
}
return
}
Column(
modifier = modifier
.fillMaxSize()
@ -88,6 +113,20 @@ internal fun AccessCode(
}
}
}
val hapticManager = LocalHapticManager.current
LaunchedEffect(state.accessCodeColor) {
when (state.accessCodeColor) {
PinTextColor.WrongCode -> {
launch(NonCancellable) {
delay(timeMillis = 500)
hapticManager.perform(TangemHapticEffect.View.Reject)
}
}
else -> Unit
}
}
}
@Preview(showBackground = true, widthDp = 360)
@ -101,6 +140,7 @@ private fun PreviewSet() {
accessCodeColor = PinTextColor.Primary,
onAccessCodeChange = {},
isConfirmMode = false,
isLoading = false,
),
)
}
@ -117,6 +157,23 @@ private fun PreviewConfirm() {
accessCodeColor = PinTextColor.Success,
onAccessCodeChange = {},
isConfirmMode = true,
isLoading = false,
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun PreviewConfirmLoading() {
TangemThemePreview {
AccessCode(
state = AccessCodeUM(
accessCode = "123456",
accessCodeColor = PinTextColor.Success,
onAccessCodeChange = {},
isConfirmMode = true,
isLoading = true,
),
)
}

View file

@ -123,12 +123,16 @@ internal class AddExistingWalletModel @Inject constructor(
}
inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback {
var canSkip = true
override fun onBackClick() {
onChildBack()
}
override fun onSkipClick() {
showSkipAccessCodeWarningDialog()
if (canSkip) {
showSkipAccessCodeWarningDialog()
}
}
}
@ -157,7 +161,12 @@ internal class AddExistingWalletModel @Inject constructor(
stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(userWalletId, accessCode))
}
override fun onAccessCodeUpdateStarted(userWalletId: UserWalletId) {
hotWalletStepperComponentModelCallback.canSkip = false
}
override fun onAccessCodeUpdated(userWalletId: UserWalletId) {
hotWalletStepperComponentModelCallback.canSkip = true
navigateToPushNotificationsOrNext()
}
}

View file

@ -78,7 +78,8 @@ internal class CreateHardwareWalletModel @Inject constructor(
private fun onBuyTangemWalletClick() {
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateWallet))
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
generateBuyTangemCardLinkUseCase
.invoke(GenerateBuyTangemCardLinkUseCase.Source.Upgrade).let { urlOpener.openUrl(it) }
}
}

View file

@ -50,6 +50,10 @@ internal class UpdateAccessCodeModel @Inject constructor(
stackNavigation.push(UpdateAccessCodeRoute.ConfirmAccessCode(userWalletId, accessCode))
}
override fun onAccessCodeUpdateStarted(userWalletId: UserWalletId) {
// No-op
}
override fun onAccessCodeUpdated(userWalletId: UserWalletId) {
stackNavigation.push(UpdateAccessCodeRoute.SetupFinished)
}

View file

@ -94,7 +94,8 @@ internal class UpgradeWalletModel @Inject constructor(
private fun onBuyTangemWalletClick() {
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Upgrade))
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
generateBuyTangemCardLinkUseCase
.invoke(GenerateBuyTangemCardLinkUseCase.Source.Upgrade).let { urlOpener.openUrl(it) }
}
}

View file

@ -156,12 +156,16 @@ internal class WalletActivationModel @Inject constructor(
}
inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback {
var canSkip = true
override fun onBackClick() {
onChildBack()
}
override fun onSkipClick() {
showSkipAccessCodeWarningDialog()
if (canSkip) {
showSkipAccessCodeWarningDialog()
}
}
}
@ -220,8 +224,13 @@ internal class WalletActivationModel @Inject constructor(
stackNavigation.push(WalletActivationRoute.ConfirmAccessCode(accessCode))
}
override fun onAccessCodeUpdateStarted(userWalletId: UserWalletId) {
hotWalletStepperComponentModelCallback.canSkip = false
}
override fun onAccessCodeUpdated(userWalletId: UserWalletId) {
navigateToPushNotificationsOrNext()
hotWalletStepperComponentModelCallback.canSkip = true
}
}

View file

@ -132,7 +132,8 @@ internal class WalletBackupModel @Inject constructor(
private fun onBuyClick() {
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Backup))
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
generateBuyTangemCardLinkUseCase
.invoke(GenerateBuyTangemCardLinkUseCase.Source.Backup).let { urlOpener.openUrl(it) }
}
}

View file

@ -171,7 +171,8 @@ internal class WalletHardwareBackupModel @Inject constructor(
private fun onBuyClick() {
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.HardwareWallet))
modelScope.launch {
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
generateBuyTangemCardLinkUseCase
.invoke(GenerateBuyTangemCardLinkUseCase.Source.Backup).let { urlOpener.openUrl(it) }
}
}

View file

@ -34,6 +34,7 @@ dependencies {
/** Common */
implementation(projects.common.ui)
implementation(projects.common.routing)
implementation(projects.common)
/** Domain */
implementation(projects.domain.models)

View file

@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.mode
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.TangemBlogUrlBuilder
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
@ -161,7 +162,11 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
openGeneratedSeedPhrase()
},
onLearnMoreClicked = {
urlOpener.openUrl(seedPhraseLearnMoreUrl())
modelScope.launch {
urlOpener.openUrl(
TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.SeedPhraseRiskySolution),
)
}
},
)
}

View file

@ -1,28 +0,0 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model
import android.net.Uri
import com.tangem.utils.SupportedLanguages
import java.util.Locale
// TODO: [REDACTED_JIRA]
internal fun seedPhraseLearnMoreUrl(): String {
val language = Locale.getDefault().language
val languageBy = "by"
if (SupportedLanguages.RUSSIAN.equals(language, true) ||
languageBy.equals(language, true)
) {
SupportedLanguages.RUSSIAN
} else {
SupportedLanguages.ENGLISH
}
val webUri = Uri.Builder()
.scheme("https")
.authority("tangem.com")
.appendPath(language)
.appendPath("blog/post/seed-phrase-a-risky-solution")
.build()
return webUri.toString()
}

View file

@ -48,7 +48,7 @@ internal fun getRewardScheduleText(
-> getCustomRewardSchedule(
networkId = networkId,
decapitalize = decapitalize,
)
) ?: stringReference(rewardSchedule.name.lowercase().capitalize())
RewardSchedule.UNKNOWN -> null
}
}

View file

@ -1194,6 +1194,7 @@ internal class StateBuilder(
)
}
@Deprecated("Use TangemBlockUrlBuilder instead")
private fun buildReadMoreUrl(): String {
return buildString {
append(FEE_READ_MORE_URL_FIRST_PART)

View file

@ -100,6 +100,14 @@ internal class TangemPayDetailsComponent(
listener = model,
),
)
is TangemPayDetailsNavigation.ViewPinCode -> TangemPayViewPinComponent(
appComponentContext = context,
params = TangemPayViewPinComponent.Params(
walletId = navigation.userWalletId,
cardId = navigation.cardId,
listener = model,
),
)
}
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.features.tangempay.components
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.tangempay.model.TangemPayViewPinModel
import com.tangem.features.tangempay.ui.TangemPayViewPinContent
internal class TangemPayViewPinComponent(
appComponentContext: AppComponentContext,
params: Params,
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
private val model: TangemPayViewPinModel = getOrCreateModel(params = params)
override fun dismiss() {
model.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = ::dismiss)
DisableScreenshotsDisposableEffect()
TangemPayViewPinContent(state = state)
}
data class Params(
val listener: ViewPinListener,
val walletId: UserWalletId,
val cardId: String,
)
}
internal interface ViewPinListener {
fun onClickChangePin()
fun onDismissViewPin()
}

View file

@ -9,6 +9,7 @@ import com.tangem.features.tangempay.model.TangemPayChangePinModel
import com.tangem.features.tangempay.model.TangemPayDetailsModel
import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel
import com.tangem.features.tangempay.model.TangemPayTxHistoryModel
import com.tangem.features.tangempay.model.TangemPayViewPinModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -53,4 +54,9 @@ internal interface TangemPayModelModule {
@IntoMap
@ClassKey(TangemPayAddFundsModel::class)
fun bindTangemPayAddFundsModel(model: TangemPayAddFundsModel): Model
@Binds
@IntoMap
@ClassKey(TangemPayViewPinModel::class)
fun bindTangemPayViewPinModel(model: TangemPayViewPinModel): Model
}

View file

@ -26,4 +26,10 @@ internal sealed class TangemPayDetailsNavigation {
val transaction: TangemPayTxHistoryItem,
val isBalanceHidden: Boolean,
) : TangemPayDetailsNavigation()
@Serializable
data class ViewPinCode(
val userWalletId: UserWalletId,
val cardId: String,
) : TangemPayDetailsNavigation()
}

View file

@ -51,9 +51,9 @@ internal class TangemPayDetailsStateFactory(
TangemPayDetailsTopBarMenuItem(
type = TangemPayDetailsTopBarMenuItemType.ChangePin,
dropdownItem = TangemDropdownMenuItem(
title = resourceReference(R.string.tangempay_card_details_change_pin),
title = resourceReference(R.string.tangempay_card_details_pin_code),
textColor = themedColor { TangemTheme.colors.text.primary1 },
onClick = intents::onClickChangePin,
onClick = intents::onClickPinCode,
),
),
TangemPayDetailsTopBarMenuItem(

View file

@ -0,0 +1,23 @@
package com.tangem.features.tangempay.entity
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
internal sealed class TangemPayViewPinUM {
open val onDismiss: () -> Unit = {}
data class Loading(
override val onDismiss: () -> Unit,
) : TangemPayViewPinUM()
data class Content(
val pin: String,
val onClickChangePin: () -> Unit,
override val onDismiss: () -> Unit,
) : TangemPayViewPinUM()
data class Error(
val errorMessage: MessageBottomSheetUMV2,
override val onDismiss: () -> Unit,
) : TangemPayViewPinUM()
}

View file

@ -33,6 +33,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.tangempay.TangemPayConstants
import com.tangem.features.tangempay.components.AddFundsListener
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
import com.tangem.features.tangempay.components.ViewPinListener
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
@ -77,7 +78,7 @@ internal class TangemPayDetailsModel @Inject constructor(
private val orderRepository: CustomerOrderRepository,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener, ViewPinListener {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
@ -124,8 +125,17 @@ internal class TangemPayDetailsModel @Inject constructor(
.launchIn(modelScope)
}
override fun onClickChangePin() {
router.push(TangemPayDetailsInnerRoute.ChangePIN)
override fun onClickPinCode() {
if (!params.config.isPinSet) {
router.push(TangemPayDetailsInnerRoute.ChangePIN)
} else {
bottomSheetNavigation.activate(
TangemPayDetailsNavigation.ViewPinCode(
userWalletId = params.userWalletId,
cardId = params.config.cardId,
),
)
}
}
override fun onClickFreezeCard() {
@ -389,6 +399,15 @@ internal class TangemPayDetailsModel @Inject constructor(
bottomSheetNavigation.dismiss()
}
override fun onClickChangePin() {
bottomSheetNavigation.dismiss()
router.push(TangemPayDetailsInnerRoute.ChangePIN)
}
override fun onDismissViewPin() {
bottomSheetNavigation.dismiss()
}
override fun onTransactionClick(item: TangemPayTxHistoryItem) {
bottomSheetNavigation.activate(
configuration = TangemPayDetailsNavigation.TransactionDetails(

View file

@ -0,0 +1,59 @@
package com.tangem.features.tangempay.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.features.tangempay.components.TangemPayViewPinComponent
import com.tangem.features.tangempay.entity.TangemPayViewPinUM
import com.tangem.features.tangempay.model.transformers.TangemPayViewPinErrorStateTransformer
import com.tangem.features.tangempay.model.transformers.TangemPayViewPinSuccessStateTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class TangemPayViewPinModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val cardDetailsRepository: TangemPayCardDetailsRepository,
) : Model() {
private val params = paramsContainer.require<TangemPayViewPinComponent.Params>()
val uiState: StateFlow<TangemPayViewPinUM>
field = MutableStateFlow(getInitialState())
init {
getCardPin()
}
private fun getCardPin() {
modelScope.launch {
cardDetailsRepository.getPin(userWalletId = params.walletId, cardId = params.cardId)
.onRight { pin ->
if (!pin.isNullOrEmpty()) {
uiState.update(TangemPayViewPinSuccessStateTransformer(pin, params.listener::onClickChangePin))
} else {
uiState.update(TangemPayViewPinErrorStateTransformer())
}
}
.onLeft {
uiState.update(TangemPayViewPinErrorStateTransformer())
}
}
}
private fun getInitialState(): TangemPayViewPinUM {
return TangemPayViewPinUM.Loading(onDismiss = ::onDismiss)
}
fun onDismiss() {
params.listener.onDismissViewPin()
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.components.bottomsheets.message.icon
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
import com.tangem.core.ui.components.bottomsheets.message.onClick
import com.tangem.core.ui.components.bottomsheets.message.secondaryButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.features.tangempay.entity.TangemPayViewPinUM
import com.tangem.utils.transformer.Transformer
internal class TangemPayViewPinErrorStateTransformer : Transformer<TangemPayViewPinUM> {
override fun transform(prevState: TangemPayViewPinUM): TangemPayViewPinUM {
return TangemPayViewPinUM.Error(
errorMessage = bottomSheetMessage {
infoBlock {
icon(R.drawable.img_attention_20) {
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention
}
title = TextReference.Res(R.string.common_error)
body = TextReference.Res(R.string.common_unknown_error)
}
secondaryButton {
text = resourceReference(R.string.common_got_it)
onClick { closeBs() }
}
}.messageBottomSheetUMV2,
onDismiss = prevState.onDismiss,
)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.features.tangempay.entity.TangemPayViewPinUM
import com.tangem.utils.transformer.Transformer
internal class TangemPayViewPinSuccessStateTransformer(
private val pin: String,
private val onClickChangePin: () -> Unit,
) : Transformer<TangemPayViewPinUM> {
override fun transform(prevState: TangemPayViewPinUM): TangemPayViewPinUM {
return TangemPayViewPinUM.Content(
pin = pin,
onClickChangePin = onClickChangePin,
onDismiss = prevState.onDismiss,
)
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.features.tangempay.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
internal fun PinDigitBox(
digit: String?,
backgroundColor: Color,
borderColor: Color,
textColor: Color,
textStyle: TextStyle,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.size(width = 48.dp, height = 64.dp)
.background(
color = backgroundColor,
shape = RoundedCornerShape(12.dp),
)
.border(
width = 1.dp,
color = borderColor,
shape = RoundedCornerShape(12.dp),
),
contentAlignment = Alignment.Center,
) {
if (digit != null) {
Text(
text = digit,
style = textStyle,
color = textColor,
textAlign = TextAlign.Center,
)
}
}
}

View file

@ -1,11 +1,8 @@
package com.tangem.features.tangempay.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@ -17,11 +14,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
@ -187,38 +182,4 @@ private fun PinCode(
}
},
)
}
@Composable
private fun PinDigitBox(
digit: String?,
backgroundColor: Color,
borderColor: Color,
textColor: Color,
textStyle: TextStyle,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.size(width = 48.dp, height = 64.dp)
.background(
color = backgroundColor,
shape = RoundedCornerShape(12.dp),
)
.border(
width = 1.dp,
color = borderColor,
shape = RoundedCornerShape(12.dp),
),
contentAlignment = Alignment.Center,
) {
if (digit != null) {
Text(
text = digit,
style = textStyle,
color = textColor,
textAlign = TextAlign.Center,
)
}
}
}

View file

@ -0,0 +1,204 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color.Companion.Transparent
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetV2Content
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayViewPinUM
@Composable
internal fun TangemPayViewPinContent(state: TangemPayViewPinUM) {
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = state.onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = state.onDismiss,
containerColor = TangemTheme.colors.background.tertiary,
title = {
TangemModalBottomSheetTitle(
title = TextReference.EMPTY,
endIconRes = R.drawable.ic_close_24,
onEndClick = state.onDismiss,
)
},
) {
when (state) {
is TangemPayViewPinUM.Content -> {
PinSuccessContent(state)
}
is TangemPayViewPinUM.Error -> {
MessageBottomSheetV2Content(state.errorMessage)
}
is TangemPayViewPinUM.Loading -> {
PinLoadingContent()
}
}
}
}
@Composable
private fun PinSuccessContent(state: TangemPayViewPinUM.Content, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH12()
Text(
text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH24()
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
contentAlignment = Alignment.Center,
) {
PinCode(
modifier = Modifier
.align(Alignment.TopCenter),
value = state.pin,
)
SecondaryButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.imePadding()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.tangempay_change_pin_code),
onClick = state.onClickChangePin,
)
}
}
}
@Composable
private fun PinLoadingContent(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH12()
Text(
text = stringResourceSafe(R.string.tangempay_card_details_view_pin_code_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH24()
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
color = TangemTheme.colors.text.disabled,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 16.dp)
.size(TangemTheme.dimens.size24),
)
}
}
}
@Composable
private fun PinCode(value: String, modifier: Modifier = Modifier, numbersCount: Int = 4) {
BasicTextField(
value = value,
onValueChange = {},
modifier = modifier,
textStyle = TangemTheme.typography.h1.copy(color = Transparent),
cursorBrush = SolidColor(Transparent),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
repeat(numbersCount) { index ->
val digit = value.getOrNull(index)?.toString()
PinDigitBox(
digit = digit,
backgroundColor = TangemTheme.colors.background.action,
borderColor = TangemTheme.colors.stroke.primary,
textColor = TangemTheme.colors.text.primary1,
textStyle = TangemTheme.typography.h1,
)
}
}
innerTextField()
}
},
)
}
@Preview(showBackground = true)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayViewPinContentPreview() {
TangemThemePreview {
TangemPayViewPinContent(
state = TangemPayViewPinUM.Content(
pin = "1234",
onClickChangePin = {},
onDismiss = {},
),
)
}
}

View file

@ -7,7 +7,7 @@ internal interface TangemPayDetailIntents {
fun onRefreshSwipe(refreshState: ShowRefreshState)
fun onClickAddFunds()
fun onClickWithdraw()
fun onClickChangePin()
fun onClickPinCode()
fun onClickFreezeCard()
fun onClickUnfreezeCard()
fun onClickTermsAndLimits()

View file

@ -65,7 +65,9 @@ internal class TangemPayOnboardingModel @Inject constructor(
.onRight { isValid -> if (isValid) showOnboarding() else back() }
.onLeft { back() }
}
is TangemPayOnboardingComponent.Params.ContinueOnboarding,
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> {
openKyc(userWalletId = params.userWalletId)
}
is TangemPayOnboardingComponent.Params.FromBannerInSettings,
is TangemPayOnboardingComponent.Params.FromBannerOnMain,
-> showOnboarding()
@ -113,8 +115,13 @@ internal class TangemPayOnboardingModel @Inject constructor(
private fun onGetCardClick() {
analytics.send(TangemPayAnalyticsEvents.GetCardClicked())
// if user came from deeplink or banner in settings and already is a paera customer -> exclude this wallet
val shouldExcludePaeraCustomers = params is TangemPayOnboardingComponent.Params.FromBannerInSettings ||
params is TangemPayOnboardingComponent.Params.Deeplink
modelScope.launch {
val eligibleWalletsIds = eligibilityManager.getEligibleWallets().map { it.walletId }
val eligibleWalletsIds = eligibilityManager
.getEligibleWallets(shouldExcludePaeraCustomers = shouldExcludePaeraCustomers)
.map { it.walletId }
if (eligibleWalletsIds.isEmpty()) {
back()
return@launch

View file

@ -6,13 +6,10 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.TokenReceiveType
import com.tangem.domain.models.TokenReceiveNotification
import com.tangem.domain.models.TokenReceiveType
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.ens.EnsAddress
import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Ens
@ -111,7 +108,10 @@ internal class TokenReceiveStateFactory(
}
ReceiveAddressModel.NameService.Ens -> Ens
ReceiveAddressModel.NameService.Legacy -> Primary.Legacy(
displayName = TextReference.Res(R.string.domain_receive_assets_legacy_address),
displayName = resourceReference(
R.string.domain_receive_assets_legacy_address,
WrappedList(listOf(cryptoCurrency.name)),
),
)
}
ReceiveAddress(

View file

@ -61,7 +61,7 @@ internal class TxHistoryItemToTransactionStateConverter(
is TransactionType.Swap,
is TransactionType.Transfer,
is TransactionType.UnknownOperation,
TransactionType.YieldSupply.Send,
is TransactionType.YieldSupply.Send,
TransactionType.YieldSupply.Topup,
-> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
}
@ -83,10 +83,12 @@ internal class TxHistoryItemToTransactionStateConverter(
is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter)
is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit)
TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup)
TransactionType.YieldSupply.Send -> if (isOutgoing) {
resourceReference(R.string.common_transfer)
} else {
resourceReference(R.string.yield_module_transaction_withdraw)
is TransactionType.YieldSupply.Send -> {
if (type.isYieldSupplyWithdraw || isOutgoing) {
resourceReference(R.string.yield_module_transaction_withdraw)
} else {
resourceReference(R.string.common_transfer)
}
}
is TransactionType.YieldSupply.DeployContract -> resourceReference(
R.string
@ -107,7 +109,7 @@ internal class TxHistoryItemToTransactionStateConverter(
private fun TxInfo.extractSubtitle(): TextReference {
return when (val type = this.type) {
is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Coin) {
if (type == TransactionType.YieldSupply.Send) {
if (type is TransactionType.YieldSupply.Send) {
extractSubtitleByAddressType()
} else {
resourceReference(
@ -129,8 +131,8 @@ internal class TxHistoryItemToTransactionStateConverter(
val amount = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) }
resourceReference(R.string.yield_module_transaction_exit_subtitle, wrappedList(amount))
}
TransactionType.YieldSupply.Send -> {
if (isOutgoing) {
is TransactionType.YieldSupply.Send -> {
if (isOutgoing || !type.isYieldSupplyWithdraw) {
extractSubtitleByAddressType()
} else {
val amount =
@ -190,12 +192,9 @@ internal class TxHistoryItemToTransactionStateConverter(
TransactionType.Staking.Withdraw,
-> return ""
is TransactionType.YieldSupply -> if (currency is CryptoCurrency.Token) {
when (type) {
TransactionType.YieldSupply.Send -> if (!isOutgoing) {
return ""
}
else -> return ""
is TransactionType.YieldSupply -> {
if (currency is CryptoCurrency.Token && type == TransactionType.YieldSupply.Send && !isOutgoing) {
return ""
}
}
else -> Unit

View file

@ -121,6 +121,8 @@ internal class WalletModel @Inject constructor(
private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
init {
trackScreenOpened()
updateMarketToggle()
suggestToOpenMarkets()
@ -139,9 +141,7 @@ internal class WalletModel @Inject constructor(
}
fun onResume() {
modelScope.launch(dispatchers.main) {
suggestToEnableBiometrics()
}
suggestToEnableBiometrics()
}
private fun updateMarketToggle() {
@ -178,13 +178,15 @@ internal class WalletModel @Inject constructor(
walletScreenContentLoader.cancelAll()
}
private suspend fun suggestToEnableBiometrics() {
if (shouldShowAskBiometryBottomSheet()) {
delay(timeMillis = 1_800)
private fun suggestToEnableBiometrics() {
modelScope.launch(dispatchers.main) {
if (shouldShowAskBiometryBottomSheet()) {
delay(timeMillis = 1_800)
innerWalletRouter.dialogNavigation.activate(
configuration = WalletDialogConfig.AskForBiometry,
)
innerWalletRouter.dialogNavigation.activate(
configuration = WalletDialogConfig.AskForBiometry,
)
}
}
}
@ -202,6 +204,42 @@ internal class WalletModel @Inject constructor(
}
}
private fun trackScreenOpened() {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
modelScope.launch {
userWalletsListRepository
.selectedUserWalletSync()
?.let { selectedWallet ->
val hasMobileWallet = userWalletsListRepository.userWalletsSync()
.any { it is UserWallet.Hot }
val accountsCount = if (isAccountsModeEnabledUseCase.invokeSync()) {
singleAccountListSupplier(selectedWallet.walletId)
.first()
.accounts
.size
} else {
null
}
val result = getAppThemeModeUseCase().firstOrNull()
val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM
analyticsEventsHandler.send(
WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(
hasMobileWallet = hasMobileWallet,
accountsCount = accountsCount,
theme = theme.value,
isImported = selectedWallet.isImported(),
),
)
}
}
} else {
analyticsEventsHandler.send(
WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy(),
)
}
}
private suspend fun shouldShowAskBiometryBottomSheet(): Boolean {
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.userWalletsSync().any { it is UserWallet.Cold } &&
@ -289,31 +327,6 @@ internal class WalletModel @Inject constructor(
.onEach { selectedWallet ->
trackingContextProxy.setContext(selectedWallet)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
modelScope.launch {
val hasMobileWallet = userWalletsListRepository.userWalletsSync()
.any { it is UserWallet.Hot }
val accountsCount = if (isAccountsModeEnabledUseCase.invokeSync()) {
singleAccountListSupplier(selectedWallet.walletId)
.first()
.accounts
.size
} else {
null
}
val result = getAppThemeModeUseCase().firstOrNull()
val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM
analyticsEventsHandler.send(
WalletScreenAnalyticsEvent.MainScreen.ScreenOpened(
hasMobileWallet = hasMobileWallet,
accountsCount = accountsCount,
theme = theme.value,
isImported = selectedWallet.isImported(),
),
)
}
}
if (selectedWallet.isMultiCurrency) {
selectedWalletAnalyticsSender.send(selectedWallet)
}

View file

@ -51,6 +51,8 @@ sealed class WalletScreenAnalyticsEvent {
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Main Screen", event = event, params = params) {
class ScreenOpenedLegacy : MainScreen(event = "Screen opened")
data class ScreenOpened(
private val hasMobileWallet: Boolean,
private val accountsCount: Int?,

View file

@ -66,6 +66,7 @@ internal class TangemPayUpdateInfoStateTransformer(
openDetails(
TangemPayDetailsConfig(
cardId = productInstance.cardId,
isPinSet = cardInfo.isPinSet,
cardFrozenState = cardFrozenState,
customerWalletAddress = cardInfo.customerWalletAddress,
cardNumberEnd = cardInfo.lastFourDigits,

View file

@ -71,7 +71,7 @@ internal class TxHistoryItemStateConverter(
is TransactionType.Swap,
is TransactionType.Transfer,
is TransactionType.UnknownOperation,
TransactionType.YieldSupply.Send,
is TransactionType.YieldSupply.Send,
TransactionType.YieldSupply.Topup,
-> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24
}
@ -93,10 +93,12 @@ internal class TxHistoryItemStateConverter(
is TransactionType.YieldSupply.Enter -> resourceReference(R.string.yield_module_transaction_enter)
is TransactionType.YieldSupply.Exit -> resourceReference(R.string.yield_module_transaction_exit)
TransactionType.YieldSupply.Topup -> resourceReference(R.string.yield_module_transaction_topup)
TransactionType.YieldSupply.Send -> if (isOutgoing) {
resourceReference(R.string.common_transfer)
} else {
resourceReference(R.string.yield_module_transaction_withdraw)
is TransactionType.YieldSupply.Send -> {
if (type.isYieldSupplyWithdraw || isOutgoing) {
resourceReference(R.string.yield_module_transaction_withdraw)
} else {
resourceReference(R.string.common_transfer)
}
}
is TransactionType.YieldSupply.DeployContract -> resourceReference(
R.string
@ -137,8 +139,8 @@ internal class TxHistoryItemStateConverter(
wrappedList(amount),
)
}
TransactionType.YieldSupply.Send -> {
if (isOutgoing) {
is TransactionType.YieldSupply.Send -> {
if (isOutgoing || !type.isYieldSupplyWithdraw) {
extractSubtitleByAddressType()
} else {
val amount =

View file

@ -20,16 +20,15 @@ internal class YieldSupplyPromoBannerKeyConverter(
is TokenConverterParams.Wallet -> value.tokenList.flattenCurrencies()
is TokenConverterParams.Account -> value.accountList.flattenCurrencies()
}.filter { status ->
status.value is CryptoCurrencyStatus.Loaded ||
status.value is CryptoCurrencyStatus.Custom
status.value is CryptoCurrencyStatus.Loaded
}
val tokens = currencies.filter { it.currency is CryptoCurrency.Token }
val cryptoCurrencyStatuses = currencies.filter { it.currency is CryptoCurrency.Token }
if (tokens.any { it.value.yieldSupplyStatus?.isActive == true }) return null
if (cryptoCurrencyStatuses.any { it.value.yieldSupplyStatus?.isActive == true }) return null
if (yieldModuleApyMap.isEmpty()) return null
val max = tokens.asSequence()
val max = cryptoCurrencyStatuses.asSequence()
.mapNotNull { status ->
val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null
val tokenKey = token.yieldSupplyKey()

View file

@ -4,21 +4,9 @@ import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
@ -28,8 +16,9 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -57,28 +46,20 @@ internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, m
)
.clickable(onClick = state.onClick),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
ConstraintLayout(
modifier = Modifier.fillMaxWidth(),
) {
Image(
painter = painterResource(R.drawable.img_tangem_pay_onboarding_banner),
contentDescription = null,
modifier = Modifier
.padding(horizontal = 8.dp)
.width(55.dp)
.height(95.dp)
.offset(y = 10.dp),
)
SpacerH4()
val (image, text, close) = createRefs()
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.Center,
modifier = Modifier
.constrainAs(text) {
top.linkTo(parent.top)
start.linkTo(image.end, margin = 12.dp)
end.linkTo(parent.end)
width = Dimension.fillToConstraints
}
.padding(top = 16.dp, end = 16.dp, bottom = 16.dp),
) {
Text(
text = stringResourceSafe(R.string.tangempay_onboarding_banner_title),
@ -94,22 +75,32 @@ internal fun TangemPayOnboardingBanner(state: TangemPayState.OnboardingBanner, m
color = TangemTheme.colors.text.tertiary,
)
}
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.TopEnd,
) {
Image(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.clickable(onClick = state.closeOnClick)
.padding(4.dp)
.padding(top = 12.dp)
.size(12.dp),
painter = painterResource(id = R.drawable.ic_close_24),
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
contentDescription = null,
)
}
Image(
painter = painterResource(R.drawable.img_tangem_pay_onboarding_banner),
contentDescription = null,
modifier = Modifier
.constrainAs(image) {
start.linkTo(parent.start)
top.linkTo(text.top)
bottom.linkTo(text.bottom)
height = Dimension.fillToConstraints
}
.padding(top = 8.dp, start = 24.dp),
)
Image(
painter = painterResource(R.drawable.ic_close_24),
contentDescription = null,
modifier = Modifier
.size(16.dp)
.clickable(onClick = state.closeOnClick)
.constrainAs(close) {
top.linkTo(parent.top, margin = 16.dp)
end.linkTo(parent.end, margin = 16.dp)
},
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
)
}
}
}

View file

@ -20,7 +20,7 @@ class YieldSupplyPromoBannerKeyConverterTest {
@Test
fun `GIVEN promo disabled WHEN convert THEN return null`() {
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xABCDEF")
val status = createStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false)
val status = createLoadedStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false)
val tokenList = ungroupedTokenList(status)
val params = TokenConverterParams.Wallet(
portfolioId = PortfolioId.Wallet(UserWalletId("00")),
@ -39,7 +39,7 @@ class YieldSupplyPromoBannerKeyConverterTest {
@Test
fun `GIVEN empty apy map WHEN convert THEN return null`() {
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xA1")
val status = createStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false)
val status = createLoadedStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false)
val params = TokenConverterParams.Wallet(
portfolioId = PortfolioId.Wallet(UserWalletId("00")),
tokenList = ungroupedTokenList(status),
@ -57,7 +57,7 @@ class YieldSupplyPromoBannerKeyConverterTest {
@Test
fun `GIVEN active yield token present WHEN convert THEN return null`() {
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xAA")
val statusActive = createStatus(token = token, amount = BigDecimal("5"), isYieldActive = true)
val statusActive = createLoadedStatus(token = token, amount = BigDecimal("5"), isYieldActive = true)
val params = TokenConverterParams.Wallet(
portfolioId = PortfolioId.Wallet(UserWalletId("00")),
tokenList = ungroupedTokenList(statusActive),
@ -78,8 +78,8 @@ class YieldSupplyPromoBannerKeyConverterTest {
val tokenSmall = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xAbCd")
val tokenBig = createToken(networkId = evmNetworkId, backendId = evmNetworkId, contract = "0xBEEF")
val statusSmall = createStatus(token = tokenSmall, amount = BigDecimal("1.00"), isYieldActive = false)
val statusBig = createStatus(token = tokenBig, amount = BigDecimal("10.00"), isYieldActive = false)
val statusSmall = createLoadedStatus(token = tokenSmall, amount = BigDecimal("1.00"), isYieldActive = false)
val statusBig = createLoadedStatus(token = tokenBig, amount = BigDecimal("10.00"), isYieldActive = false)
val apyMap = mapOf(
"${tokenSmall.network.backendId}_${tokenSmall.contractAddress.lowercase()}" to BigDecimal("0.05"),
@ -105,7 +105,7 @@ class YieldSupplyPromoBannerKeyConverterTest {
fun `GIVEN non evm case sensitive mismatch WHEN convert THEN return null`() {
val nonEvmId = "xrp"
val token = createToken(networkId = nonEvmId, backendId = nonEvmId, contract = "rAbC123")
val status = createStatus(token = token, amount = BigDecimal("3"), isYieldActive = false)
val status = createLoadedStatus(token = token, amount = BigDecimal("3"), isYieldActive = false)
val mismatchedKey = "${token.network.backendId}_${token.contractAddress.lowercase()}"
val apyMap = mapOf(mismatchedKey to BigDecimal("0.07"))
@ -124,6 +124,24 @@ class YieldSupplyPromoBannerKeyConverterTest {
assertThat(result).isNull()
}
@Test
fun `GIVEN custom status WHEN convert THEN return null`() {
val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xCUSTOM")
val status = createCustomStatus(token = token, amount = BigDecimal("5.0"), isYieldActive = false)
val params = TokenConverterParams.Wallet(
portfolioId = PortfolioId.Wallet(UserWalletId("00")),
tokenList = ungroupedTokenList(status),
)
val converter = YieldSupplyPromoBannerKeyConverter(
yieldModuleApyMap = mapOf(token.yieldSupplyKey() to BigDecimal("0.10")),
shouldShowMainPromo = true,
)
val result = converter.convert(params)
assertThat(result).isNull()
}
private fun ungroupedTokenList(vararg statuses: CryptoCurrencyStatus): TokenList.Ungrouped {
return TokenList.Ungrouped(
totalFiatBalance = com.tangem.domain.models.TotalFiatBalance.Loaded(
@ -135,7 +153,45 @@ class YieldSupplyPromoBannerKeyConverterTest {
)
}
private fun createStatus(
private fun createLoadedStatus(
token: CryptoCurrency.Token,
amount: BigDecimal,
isYieldActive: Boolean,
): CryptoCurrencyStatus {
val networkAddress = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(
value = "addr",
type = NetworkAddress.Address.Type.Primary,
),
)
val value = CryptoCurrencyStatus.Loaded(
amount = amount,
fiatAmount = BigDecimal.ZERO,
fiatRate = BigDecimal.ONE,
priceChange = BigDecimal.ZERO,
stakingBalance = null,
yieldSupplyStatus = if (isYieldActive) {
YieldSupplyStatus(
isActive = true,
isInitialized = true,
isAllowedToSpend = true,
effectiveProtocolBalance = null,
)
} else {
null
},
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = networkAddress,
sources = CryptoCurrencyStatus.Sources(),
)
return CryptoCurrencyStatus(
currency = token,
value = value,
)
}
private fun createCustomStatus(
token: CryptoCurrency.Token,
amount: BigDecimal,
isYieldActive: Boolean,

View file

@ -297,6 +297,7 @@ internal class WcSendTransactionModel @Inject constructor(
stackNavigation.pop()
}
@Deprecated("Use TangemBlockUrlBuilder instead")
private fun onApproveLearnMoreClick() {
val code = SupportedLanguages.getCurrentSupportedLanguageCode()
.takeIf { it == SupportedLanguages.RUSSIAN }

View file

@ -242,7 +242,11 @@ internal class YieldSupplyActiveModel @Inject constructor(
userWalletId,
cryptoCurrencyStatusFlow.value,
).onRight { minAmount ->
val dustAmount = yieldSupplyGetDustMinAmountUseCase(minAmount = minAmount, appCurrency = appCurrency)
val dustAmount = yieldSupplyGetDustMinAmountUseCase(
minAmountTokenCurrency = minAmount,
appCurrency = appCurrency,
tokenCryptoCurrencyStatus = cryptoCurrencyStatusFlow.value,
)
uiState.update(
YieldSupplyActiveMinAmountTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value,

View file

@ -11,8 +11,8 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.notSuppliedAmountOrNull
import com.tangem.domain.models.currency.shouldShowNotSuppliedInfoIcon
import com.tangem.domain.models.currency.notSuppliedCryptoAmountOrNull
import com.tangem.domain.models.currency.shouldShowNotSuppliedNotification
import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.active.entity.YieldSupplyActiveContentUM
@ -91,11 +91,11 @@ internal class YieldSupplyActiveMinAmountTransformer(
}
private fun getNotSuppliedNotification(cryptoCurrencyStatus: CryptoCurrencyStatus): NotificationUM? {
return if (cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustMinAmount)) {
return if (cryptoCurrencyStatus.shouldShowNotSuppliedNotification(dustMinAmount)) {
val cryptoCurrency = cryptoCurrencyStatus.currency
val notDepositedAmount = cryptoCurrencyStatus.notSuppliedAmountOrNull()
val notSuppliedAmount = cryptoCurrencyStatus.notSuppliedCryptoAmountOrNull()
val formattedAmount =
notDepositedAmount.format { crypto(symbol = "", decimals = cryptoCurrencyStatus.currency.decimals) }
notSuppliedAmount.format { crypto(symbol = "", decimals = cryptoCurrencyStatus.currency.decimals) }
analyticsHandler.send(
YieldSupplyAnalytics.NoticeAmountNotDeposited(
token = cryptoCurrency.symbol,

View file

@ -19,7 +19,7 @@ import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.hasNotSuppliedAmount
import com.tangem.domain.models.currency.shouldShowNotSuppliedInfoIcon
import com.tangem.domain.models.currency.shouldShowNotSuppliedNotification
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
@ -347,10 +347,11 @@ internal class YieldSupplyModel @Inject constructor(
.getOrNull()
if (minAmount != null) {
val dustAmount = yieldSupplyGetDustMinAmountUseCase(
minAmount = minAmount,
minAmountTokenCurrency = minAmount,
appCurrency = appCurrency,
tokenCryptoCurrencyStatus = cryptoCurrencyStatus,
)
cryptoCurrencyStatus.shouldShowNotSuppliedInfoIcon(dustAmount)
cryptoCurrencyStatus.shouldShowNotSuppliedNotification(dustAmount)
} else {
false
}