diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 72762cb7a0..f486cce85c 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index d70fc8ae08..9ea074747f 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -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() } .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) } diff --git a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt index 4966d2d7b0..0319e1c59f 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt @@ -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" + } } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt new file mode 100644 index 0000000000..573b95bdfb --- /dev/null +++ b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 5b6a4dedc9..424ce82802 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -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 ) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt index 8b818cff1b..9d8e0aa9c4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt @@ -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) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardBalanceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardBalanceResponse.kt index 164915f64d..6eae94e330 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardBalanceResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardBalanceResponse.kt @@ -4,5 +4,4 @@ import com.squareup.moshi.Json data class CardBalanceResponse( @Json(name = "result") val result: BalanceResponse?, - @Json(name = "error") val error: String?, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt index 978baa11f9..17b226bacc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CardDetailsResponse.kt @@ -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) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt index b926f0fa05..01ee25582c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CheckCustomerWalletResponse.kt @@ -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?, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt index 8ddcfab3f7..48e148069c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerEligibilityResponse.kt @@ -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( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt index baf6d5d265..69405843f7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -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?, ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/DeeplinkValidityResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/DeeplinkValidityResponse.kt index 638055ef5c..71b094a2cd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/DeeplinkValidityResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/DeeplinkValidityResponse.kt @@ -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( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt index 7ec1155bdc..e42230d2fc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/FreezeUnfreezeCardResponse.kt @@ -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( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt index 806012dfa3..a476b5dc1b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt @@ -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( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt index 8b75b8634b..009b96d99c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/TangemPayTxHistoryResponse.kt @@ -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) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawDataResponse.kt index 8bc4fcbf1e..af5860ab39 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawDataResponse.kt @@ -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( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawResponse.kt index 2b347f27c0..5a353ae6bf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/WithdrawResponse.kt @@ -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( diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index faddf299c6..dea8c07645 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -896,6 +896,8 @@ 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. Les propriétaires du Tangem Ring ont droit à 3 swap gratis sur Changelly jusqu\'au 15/11 ! Échangez avec 0 % de frais ! + 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. + Accès root détecté Connectez-vous à l\'application et vérifiez votre solde sans scanner la carte Accéder à l\'application Autoriser l\'utilisation de la biométrie diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index bce005b585..70f1b95349 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -103,10 +103,10 @@ 設定に移動して、Tangemアプリで生体認証を有効にします。 生体認証を有効にする %1$sが無効になると、アプリのロックを解除してウォレットを操作するために、パスコードを入力する必要があります。 - 後でウォレットのアクセスコードを入力してもらいます。それを安全に保存し、今後の利用に備えるためです。 - これにより、保存されているウォレットアクセスコードがすべて削除されます。ウォレットでの今後の操作には、アクセスコードの送信が必要になります。 + 安全に保管するため、後ほどアクセスコードの入力を求められます。 + 保存されているすべてのウォレットのアクセスコードが削除されます。ウォレットを使用するには、再度アクセスコードを入力する必要があります。 保存したデバイスを削除すると、保存されているすべてのウォレットとそのアクセスコードがアプリから削除されます。 - これにより、保存されているウォレットアクセスコードがすべて削除されます。今後ウォレットを操作するには、アクセスコードの送信が必要になります。 + 保存されているすべてのウォレットのアクセスコードが削除されます。ウォレットを使用するには、再度アクセスコードを入力する必要があります。 アクセスコードを要求する このオプションを選択すると、機密性の高い操作における生体認証が無効になります。取引に署名するたびにアクセスコードを入力する必要があります。 アクセスコードを保存 @@ -144,10 +144,10 @@ 残高は非表示 ブロックチェーン開発者によると、Kaspaトークンは現在ベータ版です。アップデートをお楽しみに! ベータモード - デバイスで生体認証がオフになっているため、ウォレットのロック解除に使用できません。生体認証を再度利用するには、デバイスの設定で有効にしてください。 + この端末では生体認証がオフになっているため、ウォレットの解除に使用できません。再度この方法を使用するには、端末の設定で生体認証を有効にしてください。 生体認証が無効になっています カードまたはリングをスキャンしてください - 生体認証の試行回数が上限に達しました。端末タップまたはアクセスコードでウォレットを解除してください。 + 生体認証の試行回数の上限に達しました。カード/リングでウォレットを解除するか、アクセスコードを入力してください。 生体認証がロックされています 30秒後に再試行するか、カードまたはリングをスキャンしてください 生体認証ログインは一時的にロックされています。30秒後にもう一度お試しいただくか、端末タップまたはアクセスコードでウォレットを解除してください。 @@ -179,7 +179,7 @@ もう一度アップグレードしてください リセット完了 このウォレット内のすべてのTangemデバイスで、リセット処理を完了することを推奨します。 - すべてのTangemデバイスをリセットしていません + リセットが必要なTangemデバイスがあります。 このカードを使用して、このウォレット内の他のカードまたはリングのアクセスコードをリセットしたくない場合は、このオプションを無効にしてください。これにより、このカードのアクセスコードもリセットできなくなりますのでご注意ください。 このカードを使用して、このウォレット内の他のカードのアクセスコードをリセットできます。 アクセスコードの復元 @@ -378,6 +378,7 @@ 送金 データを読み込めません… わかりました + 理解して続行 エラーが発生しました。もう一度お試しください。 アクセスできません ステーキング解除 @@ -567,7 +568,7 @@ 新しいウォレットを作成する Tangemを注文 Tangemをスキャン - 「Tangem」に生体認証の使用を許可しますか?\n本人確認とアプリの起動のために使用されます。 + 「Tangem」が生体認証を使用して本人確認を行い、アプリを開くことを許可しますか? %sへ %sネットワーク アクセスコードの設定をキャンセルしてもよろしいですか? @@ -1059,6 +1060,7 @@ カードをリセットする この操作を実行すると、現在のウォレットにアクセスできなくなることを理解しています。 このカードを使用して、現在のウォレットの他のカードのアクセスコードを回復させられないことを認識しています。 + 復元は不可能であり、Tangem Payカードおよびカード上のすべての資金へのアクセスを完全に失うことを理解しました 工場出荷時設定にリセットすると、選択したカードやリングからウォレットが完全に削除されます。現在のウォレットを復元したり、カードやリングを使用してアクセスコードを復元することはできません。 工場出荷時の状態にリセットすると、選択したカードやリングからウォレットが完全に削除され、アプリから削除されます。現在のウォレットを復元することはできません。 すべてのTangemデバイスがリセットされました。 @@ -1067,6 +1069,8 @@ 続行するには次のデバイスをリセットしてください Tangem Ringユーザーは、11/15日までChangelly経由でスワップを3回手数料ゼロで行えます! 今すぐ手数料0% でスワップしましょう! + Rootアクセスが有効な端末は、セキュリティが低いと判断されます。データが追加のリスクにさらされる可能性があります。 + Rootアクセスが検出されました アプリにログインして、カードまたはリングをスキャンせずに残高を確認できます アプリにアクセスする 生体認証の使用を許可する @@ -1402,6 +1406,7 @@ カードが凍結されています サポートを受ける その他 + Root化された端末では使用できません 完了 拒否 保留中 @@ -1414,6 +1419,8 @@ カードの凍結解除に失敗しました。しばらくしてからもう一度お試しください。 カードの凍結が解除されました 出金 + Root化された端末では使用できません + KYCをキャンセル 資金を追加 入金オプション カード番号 @@ -1441,6 +1448,7 @@ 準備完了!カードはすぐに使用できます。 Google Payにカードを追加する Apple Payにカードを追加する + PINコード アドレスを共有するか、QRコードを表示してください。 技術的な問題が検出されました。しばらくしてからもう一度お試しいただくか、サポートにお問い合わせください。 現在、受け取りは利用できません @@ -1449,12 +1457,15 @@ ポートフォリオ内のあらゆる資産をカードと交換 カードの詳細 カードの一時停止を解除 + 忘れた場合は、アプリに戻ってください。 + PINコード 出金 現在、出金ができません 現在の処理が完了するまでは、スワップや新しい出金を開始することはできません。 出金処理中です PINコードを変更 忘れた場合はアプリに戻って確認できます。 + 復元は不可能であり、Tangem Payカードおよびカード上のすべての資金へのアクセスを完全に失うことを理解しました カードの発行に失敗しました 技術的なエラーが発生しました。下のボタンをクリックして、もう一度お試しください。 技術的なエラーが発生しました。サポートへお問い合わせください。 @@ -1471,6 +1482,7 @@ KYC進行中 ステータスを表示 Tangem PayのKYC手続き進行中 + 以下のボタンから、現在のKYCステータスを確認するか、KYCをキャンセルできます。 暗号資産を、リアルな支払いに。\n他とはまったく違う、新しいタイプの決済カード。 Tangem Visaカード カードをGET @@ -1488,7 +1500,7 @@ 現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。 同期が必要です Tangem Visaカード - Tangem Payは一時的に利用できません + Tangem Payは現在一時的に利用できません。 Tangem Pay カードまたはリングを使用して、支払いアカウントへのアクセスを復元してください。 PINコード @@ -1572,7 +1584,7 @@ プッシュ通知を使用しますか? プッシュ通知を有効にすると、ウォレットに着金したときにアラートを受信できます。 取引を見逃さない - 新しいウォレットを追加 + ウォレットを追加 バックアップせずにこのウォレットを削除すると、資金に永久にアクセスできなくなります。 このウォレットを忘れてもよろしいですか? エラーが発生しました。カードまたはリングをスキャンしてログインしてください。 @@ -1879,7 +1891,7 @@ 不審な取引 すでにTangemウォレットをお持ちですか? 数千種類の資産 - 業界最高水準のハードウェアウォレット + 最高水準のハードウェアウォレット 迅速な配送 ワンタップで開始 シームレスで安全 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 03ce372d0f..036b504b45 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -394,6 +394,7 @@ Перевод Невозможно загрузить данные… Я понял + Я понимаю, продолжить Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно Завершить стейкинг @@ -733,6 +734,7 @@ Лидеры роста Лидеры падения В тренде + Режим доходности Стейкинг — простой способ получать доход с вашей криптовалюты. %s Получайте до %s APY Токен добавлен @@ -994,6 +996,7 @@ Другие валюты Популярные фиаты Поиск по валюте + Эта транзакция уже была обработана. Дополнительных действий не требуется. Получение лучших курсов... Моментально Пользуясь сервисом покупки, вы соглашаетесь с %1$s и %2$s @@ -1104,6 +1107,8 @@ Сбросьте следующее устройство для продолжения. Владельцам колец — 3 обмена без комиссии на Changelly до 15.11! Обмен с 0% комиссией! + Устройства с root-доступом считаются менее безопасными. Ваши данные могут быть подвержены дополнительным рискам. + Обнаружен root-доступ Войдите в приложение и следите за своим балансом без сканирования карты или кольца Доступ в приложение Использовать биометрию @@ -1493,6 +1498,7 @@ Вывод выполняется Изменить PIN-код Можно посмотреть здесь, если забудете его. + Я понимаю, что полностью потеряю доступ к своей карте Tangem Pay и ко всем средствам на ней без возможности восстановления. Не удалось выпустить карту Техническая ошибка, попробуйте ещё раз, нажав кнопку ниже Техническая ошибка, свяжитесь с поддержкой @@ -1964,6 +1970,7 @@ Политика комиссий за пополнение Tangem взимает комиссию за обслуживание в размере 15% от полученного дохода. Ваши средства будут автоматически переведены в Aave, как только комиссия сети снизится или баланс достигнет минимально необходимой суммы. + Комиссии сейчас выше обычного из-за высокой активности на рынке. Вы можете продолжить сейчас или вернуться позже, когда комиссии снизятся. Высокая сетевая комиссия Историческая доходность Получай до %1$s APY на свой баланс diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index c6ef91ce1f..eaddf5b46e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1444,6 +1444,7 @@ Your card is unfrozen. Withdrawal Unable to use on rooted device + Cancel KYC Add funds Top-up options Card Number @@ -1471,6 +1472,7 @@ All set! Your card is ready to use. Add card to Google Pay Add card to Apple Pay + PIN code Share your address or show QR-code Technical issues detected. Please try again later or contact support. Receive unavailable now @@ -1479,6 +1481,8 @@ Swap any asset in your portfolio for card Card details Unfreeze Card + Come back to the app if you forget it. + Your PIN code Withdraw Withdraw unavailable now You can\'t initiate swap or new withdrawal till the current one is finished @@ -1502,6 +1506,7 @@ KYC in progress View Status KYC in progress for Tangem Pay + Use the buttons below to view your current KYC status or cancel it. Use your crypto for real world spending. \nIt’s a payment card unlike any other. Tangem Visa Card Get card diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt index 31ddd112b2..b949061451 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/YieldSupplyPromoBanner.kt @@ -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, ) { diff --git a/core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt b/core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt index b85a35b477..420547d13c 100644 --- a/core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt +++ b/core/utils/src/main/java/com/tangem/utils/SupportedLanguages.kt @@ -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( diff --git a/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt b/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt index 5d8f4ba63f..d24267a527 100644 --- a/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt +++ b/core/utils/src/main/java/com/tangem/utils/TangemBlogUrlBuilder.kt @@ -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" diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt index 2a04a06ae3..4a7f708311 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt @@ -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> = either { diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt index 97a6d9e7ca..f2d850f463 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt @@ -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? = null - private var eligibleWalletsDeferred: Deferred>? = null + private var cachedEligibleWallets: List? = null + private var eligibleWalletsDeferred: Deferred>? = null private val loadMutex = Mutex() + private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default) - override suspend fun getEligibleWallets(): List { + init { + resetDataWhenWalletsUpdate() + } + + override suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List { + return getUserWalletsData().mapNotNull { + if (!it.isPaeraCustomer || !shouldExcludePaeraCustomers) it.userWallet else null + } + } + + private suspend fun getUserWalletsData(): List { 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.excludePaeraCustomers(): List { - if (isEmpty()) return this + private suspend fun List.addPaeraCustomersData(): List { + 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, + ) } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 348ae72b3b..e11aa1c09b 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -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 diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 6bb55b562c..a05f92ee7a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -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 { + 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 { 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 diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index 83e30d675e..95b6decdf8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -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") } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt index 4fa2681263..ce23bc2504 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/RainCryptoUtil.kt @@ -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 } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt index 1b81ab80eb..23ee03289a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/TangemPayErrorConverter.kt @@ -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 diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index 7e669ccebb..842ce80393 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -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) } diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt index 1a677b9cdb..51d7e2fd6e 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionHistoryItemConverter.kt @@ -8,9 +8,13 @@ import com.tangem.blockchain.transactionhistory.models.TransactionHistoryItem as internal class SdkTransactionHistoryItemConverter( smartContractMethods: Map, + yieldSupplyAddresses: Set, ) : Converter { - 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" }, ) diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt index 5e9fe1dc0b..639350d503 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/SdkTransactionTypeConverter.kt @@ -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, -) : Converter, TxInfo.TransactionType> { + private val yieldSupplyAddresses: Set, +) : Converter { - override fun convert(value: Pair): 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( diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/YieldSupplyAddresses.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/YieldSupplyAddresses.kt new file mode 100644 index 0000000000..22e28e3eb6 --- /dev/null +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/YieldSupplyAddresses.kt @@ -0,0 +1,113 @@ +package com.tangem.data.walletmanager.utils + +// Remove after moving this data to BE +internal val YIELD_SUPPLY_ADDRESSES: Set = 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", +) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt index 09c43362de..cc408905ee 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt @@ -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 diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt index d8fd074625..aae35a95f1 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt @@ -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 diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensionsTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensionsTest.kt new file mode 100644 index 0000000000..402352cc42 --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensionsTest.kt @@ -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(), + ) + + val result = status.hasNotSuppliedAmount() + + assertThat(result).isFalse() + } + + @Test + fun `GIVEN yieldSupplyStatus is null WHEN hasNotSuppliedAmount THEN returns false`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + 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(), + 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(), + 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(), + 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(), + 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(), + 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(), + 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(), + ) + + 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(), + 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(), + 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(), + 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(), + 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(), + 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(), + 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(), + 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(), + 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(), + ) + + val result = status.notSuppliedCryptoAmountOrNull() + + assertThat(result).isNull() + } + + @Test + fun `GIVEN yieldSupplyStatus is null WHEN notSuppliedCryptoAmountOrNull THEN returns null`() { + val status = createCryptoCurrencyStatus( + currency = mockk(), + 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(), + 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(), + 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(), + 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(), + 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 { + every { this@mockk.yieldSupplyStatus } returns yieldSupplyStatus + every { this@mockk.amount } returns amount + every { this@mockk.fiatRate } returns fiatRate + } + return CryptoCurrencyStatus(currency = currency, value = value) + } +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt index da3de9f475..88f7b5556c 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt @@ -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, diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt index 4321710d19..412045caff 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt @@ -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 { diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt index b68cd25513..5ad9a9d3d9 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt @@ -4,5 +4,5 @@ import com.tangem.domain.models.wallet.UserWallet interface TangemPayEligibilityManager { - suspend fun getEligibleWallets(): List + suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index d747e41066..234366b079 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -32,5 +32,6 @@ data class CustomerInfo( val currencyCode: String, val customerWalletAddress: String, val depositAddress: String?, + val isPinSet: Boolean, ) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt index 73a4b8e035..088b34d193 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt @@ -15,6 +15,8 @@ interface TangemPayCardDetailsRepository { suspend fun revealCardDetails(userWalletId: UserWalletId): Either + suspend fun getPin(userWalletId: UserWalletId, cardId: String): Either + suspend fun setPin(userWalletId: UserWalletId, pin: String): Either suspend fun isAddToWalletDone(userWalletId: UserWalletId): Either diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 9636a5136b..16ebd064e6 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -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> { diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index a5779a9b3f..b7b3798d96 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -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) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt index 7c80707cb6..7a7cac1d2b 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt @@ -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"), } } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt index 7814d60add..54ee458d9d 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCase.kt @@ -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) } } diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt index 501e6c74f5..ccb1f53f0d 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyMinAmountUseCase.kt @@ -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, diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt index 50a00ac69c..67c35ae602 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt @@ -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", + ) } } \ No newline at end of file diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index 4f1fb170a2..458b4b5048 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { /** Common */ implementation(projects.common.ui) implementation(projects.common.routing) + implementation(projects.common) /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt index 2ee891e036..e3eab1b7e1 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt @@ -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)) } } diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt index 98cbe9a90b..2b1adb4ca1 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/entity/CreateWalletSelectionUM.kt @@ -10,6 +10,7 @@ internal data class CreateWalletSelectionUM( val blocks: ImmutableList, val onBackClick: () -> Unit, val onBuyClick: () -> Unit, + val onWhatToChooseClick: () -> Unit, ) { data class Block( val title: TextReference, diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index cddcad417a..37e1523bed 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -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 = { }, ), ) } diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt index ef8ef65b13..8c7bb0d2eb 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -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) } } } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index 69fe323b69..b730125b22 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -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) } } diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index 1dbc4c7a1f..e650912dc6 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -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) } } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt index 08693225cc..b9badf2e25 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt @@ -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) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index 13f1a7ab8f..1da4a1c296 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -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) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt index 978071789b..5b4302d4bf 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt @@ -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 = consumedEvent(), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index ecfa44d9d6..f92f63bdfe 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -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, ), ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index ed1d37d3f9..dc5afd1b9f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -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() } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt index 2d4b71b076..0373179468 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt @@ -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) } } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt index cd562b219b..e671521f5d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt @@ -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) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt index 7b675b8238..c7252d49c4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt @@ -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) } } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index 129947fdb7..2a1f014021 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -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 } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index f1a729835a..bdc95378ca 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -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) } } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt index 31d5ebdf25..efcd39aa14 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt @@ -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) } } } diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 7febfb81ba..a992e7daea 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { /** Common */ implementation(projects.common.ui) implementation(projects.common.routing) + implementation(projects.common) /** Domain */ implementation(projects.domain.models) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt index 0f18033dd3..3468f7a11c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt @@ -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), + ) + } }, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt deleted file mode 100644 index dbfeee4003..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/Utils.kt +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt index 7abfa03318..39bcf9c206 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingRewardsUtils.kt @@ -48,7 +48,7 @@ internal fun getRewardScheduleText( -> getCustomRewardSchedule( networkId = networkId, decapitalize = decapitalize, - ) + ) ?: stringReference(rewardSchedule.name.lowercase().capitalize()) RewardSchedule.UNKNOWN -> null } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 64ac9aa044..6ab1d4af06 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1194,6 +1194,7 @@ internal class StateBuilder( ) } + @Deprecated("Use TangemBlockUrlBuilder instead") private fun buildReadMoreUrl(): String { return buildString { append(FEE_READ_MORE_URL_FIRST_PART) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index 44a23b3ed8..fb099938ee 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -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, + ), + ) } } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayViewPinComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayViewPinComponent.kt new file mode 100644 index 0000000000..f12eb316e9 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayViewPinComponent.kt @@ -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() +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt index 2d4297613c..2150e54279 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt @@ -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 } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt index 86009f09f0..5f10a9ed77 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsNavigation.kt @@ -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() } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt index 9853165dad..55b429059a 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt @@ -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( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayViewPinUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayViewPinUM.kt new file mode 100644 index 0000000000..8ee03d3db4 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayViewPinUM.kt @@ -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() +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index df186cb3f8..2c2b69be13 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -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( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayViewPinModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayViewPinModel.kt new file mode 100644 index 0000000000..14631dc022 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayViewPinModel.kt @@ -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() + + val uiState: StateFlow + 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() + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt new file mode 100644 index 0000000000..eacc9656fd --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinErrorStateTransformer.kt @@ -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 { + + 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, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinSuccessStateTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinSuccessStateTransformer.kt new file mode 100644 index 0000000000..9258baaf5a --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayViewPinSuccessStateTransformer.kt @@ -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 { + + override fun transform(prevState: TangemPayViewPinUM): TangemPayViewPinUM { + return TangemPayViewPinUM.Content( + pin = pin, + onClickChangePin = onClickChangePin, + onDismiss = prevState.onDismiss, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/PinDigitBox.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/PinDigitBox.kt new file mode 100644 index 0000000000..ece1240de8 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/PinDigitBox.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt index f052057177..c1eaca6abc 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreen.kt @@ -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, - ) - } - } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayViewPinContent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayViewPinContent.kt new file mode 100644 index 0000000000..bcd40588a5 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayViewPinContent.kt @@ -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( + 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 = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt index 912bfefbd5..9e96a75104 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayDetailIntents.kt @@ -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() diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index 764ff69042..83700ab313 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -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 diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt index ee3dd1a028..bcd955a4af 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt @@ -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( diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt index bc5f160b47..21045da86e 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionStateConverter.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index b136451f60..c4b8aeb87c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -121,6 +121,8 @@ internal class WalletModel @Inject constructor( private var expressTxStatusTaskScheduler = SingleTaskScheduler() 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) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index dec57f0b23..c54ebcef16 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -51,6 +51,8 @@ sealed class WalletScreenAnalyticsEvent { params: Map = 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?, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt index f9127d04ef..a47a0aec3a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt @@ -66,6 +66,7 @@ internal class TangemPayUpdateInfoStateTransformer( openDetails( TangemPayDetailsConfig( cardId = productInstance.cardId, + isPinSet = cardInfo.isPinSet, cardFrozenState = cardFrozenState, customerWalletAddress = cardInfo.customerWalletAddress, cardNumberEnd = cardInfo.lastFourDigits, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt index a30612c67e..b04f228e55 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt @@ -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 = diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt index dd2569e500..f335bb30d1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverter.kt @@ -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() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt index b57061cb36..62e59a5eed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayOnboardingBanner.kt @@ -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), + ) } } } diff --git a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt index bca8b45d6e..8584c74aac 100644 --- a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt +++ b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerKeyConverterTest.kt @@ -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, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 611010e6f2..d305e4a233 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -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 } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt index e18a49c84a..3c65576a24 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt @@ -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, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt index 54dd971067..6350642aae 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/transformers/YieldSupplyActiveMinAmountTransformer.kt @@ -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, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index 25e0ab888d..17fba6e4c0 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -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 }