Updated on 2026-08-14
This commit is contained in:
commit
4f69db901e
100 changed files with 1875 additions and 378 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -4,5 +4,4 @@ import com.squareup.moshi.Json
|
|||
|
||||
data class CardBalanceResponse(
|
||||
@Json(name = "result") val result: BalanceResponse?,
|
||||
@Json(name = "error") val error: String?,
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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. \nIt’s 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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -32,5 +32,6 @@ data class CustomerInfo(
|
|||
val currencyCode: String,
|
||||
val customerWalletAddress: String,
|
||||
val depositAddress: String?,
|
||||
val isPinSet: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ dependencies {
|
|||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.common)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(projects.libs.tangemSdkApi)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ dependencies {
|
|||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.common)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ internal fun getRewardScheduleText(
|
|||
-> getCustomRewardSchedule(
|
||||
networkId = networkId,
|
||||
decapitalize = decapitalize,
|
||||
)
|
||||
) ?: stringReference(rewardSchedule.name.lowercase().capitalize())
|
||||
RewardSchedule.UNKNOWN -> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1194,6 +1194,7 @@ internal class StateBuilder(
|
|||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use TangemBlockUrlBuilder instead")
|
||||
private fun buildReadMoreUrl(): String {
|
||||
return buildString {
|
||||
append(FEE_READ_MORE_URL_FIRST_PART)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ internal class TangemPayUpdateInfoStateTransformer(
|
|||
openDetails(
|
||||
TangemPayDetailsConfig(
|
||||
cardId = productInstance.cardId,
|
||||
isPinSet = cardInfo.isPinSet,
|
||||
cardFrozenState = cardFrozenState,
|
||||
customerWalletAddress = cardInfo.customerWalletAddress,
|
||||
cardNumberEnd = cardInfo.lastFourDigits,
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue