From d05a6271de02ec73472ea86c2fdbfc49c607b355 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Nov 2023 17:49:08 +0200 Subject: [PATCH 001/139] Updated on 2026-08-14 --- features/swap/data/build.gradle.kts | 2 + .../com/tangem/feature/swap/ExpressApi.kt | 64 +++++++++++++++++++ .../swap/models/request/AssetsRequestBody.kt | 7 ++ .../swap/models/request/LeastTokenInfo.kt | 11 ++++ .../swap/models/request/PairsRequestBody.kt | 11 ++++ .../feature/swap/models/response/Asset.kt | 29 +++++++++ .../models/response/ExchangeDataResponse.kt | 41 ++++++++++++ .../swap/models/response/ExchangeProvider.kt | 28 ++++++++ .../models/response/ExchangeQuoteResponse.kt | 12 ++++ .../response/ExchangeResultsResponse.kt | 42 ++++++++++++ .../feature/swap/models/response/SwapPair.kt | 31 +++++++++ 11 files changed, 278 insertions(+) create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/ExpressApi.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/models/request/AssetsRequestBody.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/models/request/LeastTokenInfo.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/models/request/PairsRequestBody.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/models/response/Asset.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeDataResponse.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeProvider.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeQuoteResponse.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeResultsResponse.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/models/response/SwapPair.kt diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index f91ab4c7c0..67d016525d 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -15,6 +15,8 @@ dependencies { /** Network */ implementation(deps.retrofit) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) /** Domain */ implementation(projects.domain.tokens.models) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/ExpressApi.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/ExpressApi.kt new file mode 100644 index 0000000000..7fff572429 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/ExpressApi.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.swap + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.feature.swap.models.request.AssetsRequestBody +import com.tangem.feature.swap.models.request.PairsRequestBody +import com.tangem.feature.swap.models.response.* +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.Query +import java.math.BigDecimal + +/** + * Interface of Tangem Express API (new swap mechanism) + */ +internal interface ExpressApi { + + // TODO move first three params to retrofit interceptor + @POST("assets") + suspend fun getAssets( + @Header("api-key") apiKey: String, + @Header("user-id") userId: String, + @Header("session-id") sessionId: String, + @Body body: AssetsRequestBody, + ): ApiResponse> + + @POST("pairs") + suspend fun getPairs( + @Body body: PairsRequestBody, + ): ApiResponse> + + @GET("providers") + suspend fun getProviders(): ApiResponse> + + @GET("exchange-quote") + suspend fun getExchangeQuote( + @Query("fromContractAddress") fromContractAddress: String, + @Query("fromNetwork") fromNetwork: String, + @Query("toContractAddress") toContractAddress: String, + @Query("toNetwork") toNetwork: String, + @Query("fromAmount") fromAmount: BigDecimal, + @Query("providerId") providerId: Int, + @Query("rateType") rateType: RateType, + ): ApiResponse + + @GET("exchange-data") + suspend fun getExchangeData( + @Query("fromContractAddress") fromContractAddress: String, + @Query("fromNetwork") fromNetwork: String, + @Query("toContractAddress") toContractAddress: String, + @Query("toNetwork") toNetwork: String, + @Query("fromAmount") fromAmount: BigDecimal, + @Query("providerId") providerId: Int, + @Query("rateType") rateType: RateType, + @Query("toAddress") toAddress: String, + ): ApiResponse + + @GET("exchange-results") + suspend fun getExchangeResults( + @Query("txId") txId: String, + ): ApiResponse + +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/AssetsRequestBody.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/AssetsRequestBody.kt new file mode 100644 index 0000000000..9592830c3c --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/AssetsRequestBody.kt @@ -0,0 +1,7 @@ +package com.tangem.feature.swap.models.request + +import com.squareup.moshi.Json + +internal data class AssetsRequestBody( + @Json(name = "filter") val filter: List?, +) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/LeastTokenInfo.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/LeastTokenInfo.kt new file mode 100644 index 0000000000..7c6776fc82 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/LeastTokenInfo.kt @@ -0,0 +1,11 @@ +package com.tangem.feature.swap.models.request + +import com.squareup.moshi.Json + +internal data class LeastTokenInfo( + @Json(name = "contractAddress") + val contractAddress: String, + + @Json(name = "network") + val network: String, +) \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/PairsRequestBody.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/PairsRequestBody.kt new file mode 100644 index 0000000000..98ca56b52f --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/PairsRequestBody.kt @@ -0,0 +1,11 @@ +package com.tangem.feature.swap.models.request + +import com.squareup.moshi.Json + +internal data class PairsRequestBody( + @Json(name = "from") + val from: List, + + @Json(name = "to") + val to: List, +) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/Asset.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/Asset.kt new file mode 100644 index 0000000000..8d32b5d500 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/Asset.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.swap.models.response + +import com.squareup.moshi.Json + +data class Asset( + @Json(name = "contractAddress") + val contractAddress: String, + + @Json(name = "network") + val network: String, + + @Json(name = "token") + val token: String, + + @Json(name = "name") + val name: String, + + @Json(name = "symbol") + val symbol: String, + + @Json(name = "decimals") + val decimals: Int, + + @Json(name = "isActive") + val isActive: Boolean, + + @Json(name = "exchangeAvailable") + val exchangeAvailable: Boolean +) \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeDataResponse.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeDataResponse.kt new file mode 100644 index 0000000000..96ccbd9b14 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeDataResponse.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.swap.models.response + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class ExchangeDataResponse( + @Json(name = "toAmount") + val toAmount: BigDecimal, + + @Json(name = "txType") + val txType: TxType, + + @Json(name = "txId") + val txId: String, // inner tangem-express transaction id + + @Json(name = "txFrom") + val txFrom: String?, // account for debiting tokens (same as toAddress) if DEX, null if CEX + + @Json(name = "txTo") + val txTo: String, // swap smart-contract address if DEX, address for sending transaction if CEX + + @Json(name = "txData") + val txData: String?, // transaction data if DEX, null if CEX + + @Json(name = "txValue") + val txValue: BigDecimal, // amount (same as fromAmount) + + @Json(name = "externalTxId") + val externalTxId: String?, // null if DEX, provider transaction id if CEX + + @Json(name = "externalTxUrl") + val externalTxUrl: String?, // null if DEX, url of provider exchange status page if CEX +) + +enum class TxType { + @Json(name = "send") + SEND, + + @Json(name = "swap") + SWAP, +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeProvider.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeProvider.kt new file mode 100644 index 0000000000..432d4cff79 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeProvider.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.swap.models.response + +import com.squareup.moshi.Json + +data class ExchangeProvider( + @Json(name = "id") + val id: Int, + + @Json(name = "name") + val name: String, + + @Json(name = "id") + val type: ExchangeProviderType, + + @Json(name = "imageLarge") + val imageLargeUrl: Int, + + @Json(name = "imageSmall") + val imageSmallUrl: Int, +) + +enum class ExchangeProviderType { + @Json(name = "dex") + DEX, + + @Json(name = "cex") + CEX, +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeQuoteResponse.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeQuoteResponse.kt new file mode 100644 index 0000000000..686c0e51f6 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeQuoteResponse.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.swap.models.response + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class ExchangeQuoteResponse( + @Json(name = "toAmount") + val toAmount: BigDecimal, + + @Json(name = "allowanceContract") + val allowanceContract: String?, +) \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeResultsResponse.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeResultsResponse.kt new file mode 100644 index 0000000000..6b4db40e4a --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeResultsResponse.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.swap.models.response + +import com.squareup.moshi.Json + +data class ExchangeResultsResponse( + @Json(name = "status") + val status: ExchangeResultsStatus, + + @Json(name = "externalStatus") + val externalStatus: String, + + @Json(name = "externalTxUrl") + val externalTxUrl: String, + + @Json(name = "error") + val error: ExchangeResultsError?, +) + +enum class ExchangeResultsStatus { + @Json(name = "processing") + PROCESSING, + + @Json(name = "done") + DONE, + + @Json(name = "failed") + FAILED, + + @Json(name = "refunded") + REFUNDED, + + @Json(name = "verificationRequired") + VERIFICATION_REQUIRED, +} + +data class ExchangeResultsError( + @Json(name = "code") + val code: Int, + + @Json(name = "description") + val description: String, +) \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/SwapPair.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/SwapPair.kt new file mode 100644 index 0000000000..4ab2642970 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/SwapPair.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.swap.models.response + +import com.squareup.moshi.Json + +data class SwapPair( + @Json(name = "from") + val from: String, + + @Json(name = "to") + val to: String, + + @Json(name = "providers") + val providers: List, + +) + +data class SwapPairProvider( + @Json(name = "providerId") + val providerId: Int, + + @Json(name = "providerId") + val rateType: RateType, +) + +enum class RateType { + @Json(name = "float") + FLOAT, + + @Json(name = "fixed") + FIXED, +} \ No newline at end of file From ae8593b73b65a0f95a489f9c09fc69f4900cbee0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Nov 2023 18:50:03 +0200 Subject: [PATCH 002/139] Updated on 2026-08-14 --- .../com/tangem/datasource/api/express}/ExpressApi.kt | 10 +++++----- .../api/express}/models/request/AssetsRequestBody.kt | 4 ++-- .../api/express}/models/request/LeastTokenInfo.kt | 4 ++-- .../api/express}/models/request/PairsRequestBody.kt | 4 ++-- .../datasource/api/express}/models/response/Asset.kt | 2 +- .../express}/models/response/ExchangeDataResponse.kt | 2 +- .../api/express}/models/response/ExchangeProvider.kt | 2 +- .../express}/models/response/ExchangeQuoteResponse.kt | 2 +- .../models/response/ExchangeResultsResponse.kt | 2 +- .../api/express}/models/response/SwapPair.kt | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) rename {features/swap/data/src/main/java/com/tangem/feature/swap => core/datasource/src/main/java/com/tangem/datasource/api/express}/ExpressApi.kt (87%) rename {features/swap/data/src/main/java/com/tangem/feature/swap => core/datasource/src/main/java/com/tangem/datasource/api/express}/models/request/AssetsRequestBody.kt (52%) rename {features/swap/data/src/main/java/com/tangem/feature/swap => core/datasource/src/main/java/com/tangem/datasource/api/express}/models/request/LeastTokenInfo.kt (65%) rename {features/swap/data/src/main/java/com/tangem/feature/swap => core/datasource/src/main/java/com/tangem/datasource/api/express}/models/request/PairsRequestBody.kt (64%) rename {features/swap/data/src/main/java/com/tangem/feature/swap => core/datasource/src/main/java/com/tangem/datasource/api/express}/models/response/Asset.kt (89%) rename {features/swap/data/src/main/java/com/tangem/feature/swap => core/datasource/src/main/java/com/tangem/datasource/api/express}/models/response/ExchangeDataResponse.kt (94%) rename {features/swap/data/src/main/java/com/tangem/feature/swap => core/datasource/src/main/java/com/tangem/datasource/api/express}/models/response/ExchangeProvider.kt (88%) rename {features/swap/data/src/main/java/com/tangem/feature/swap => core/datasource/src/main/java/com/tangem/datasource/api/express}/models/response/ExchangeQuoteResponse.kt (79%) rename {features/swap/data/src/main/java/com/tangem/feature/swap => core/datasource/src/main/java/com/tangem/datasource/api/express}/models/response/ExchangeResultsResponse.kt (92%) rename {features/swap/data/src/main/java/com/tangem/feature/swap => core/datasource/src/main/java/com/tangem/datasource/api/express}/models/response/SwapPair.kt (88%) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/ExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt similarity index 87% rename from features/swap/data/src/main/java/com/tangem/feature/swap/ExpressApi.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt index 7fff572429..2ae9231c8f 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/ExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt @@ -1,9 +1,9 @@ -package com.tangem.feature.swap +package com.tangem.datasource.api.express import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.feature.swap.models.request.AssetsRequestBody -import com.tangem.feature.swap.models.request.PairsRequestBody -import com.tangem.feature.swap.models.response.* +import com.tangem.datasource.api.express.models.request.AssetsRequestBody +import com.tangem.datasource.api.express.models.request.PairsRequestBody +import com.tangem.datasource.api.express.models.response.* import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header @@ -14,7 +14,7 @@ import java.math.BigDecimal /** * Interface of Tangem Express API (new swap mechanism) */ -internal interface ExpressApi { +interface ExpressApi { // TODO move first three params to retrofit interceptor @POST("assets") diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/AssetsRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt similarity index 52% rename from features/swap/data/src/main/java/com/tangem/feature/swap/models/request/AssetsRequestBody.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt index 9592830c3c..e1f5629b05 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/AssetsRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt @@ -1,7 +1,7 @@ -package com.tangem.feature.swap.models.request +package com.tangem.datasource.api.express.models.request import com.squareup.moshi.Json -internal data class AssetsRequestBody( +data class AssetsRequestBody( @Json(name = "filter") val filter: List?, ) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/LeastTokenInfo.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/LeastTokenInfo.kt similarity index 65% rename from features/swap/data/src/main/java/com/tangem/feature/swap/models/request/LeastTokenInfo.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/LeastTokenInfo.kt index 7c6776fc82..fd636c6682 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/LeastTokenInfo.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/LeastTokenInfo.kt @@ -1,8 +1,8 @@ -package com.tangem.feature.swap.models.request +package com.tangem.datasource.api.express.models.request import com.squareup.moshi.Json -internal data class LeastTokenInfo( +data class LeastTokenInfo( @Json(name = "contractAddress") val contractAddress: String, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/PairsRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt similarity index 64% rename from features/swap/data/src/main/java/com/tangem/feature/swap/models/request/PairsRequestBody.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt index 98ca56b52f..32c4b5f278 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/models/request/PairsRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt @@ -1,8 +1,8 @@ -package com.tangem.feature.swap.models.request +package com.tangem.datasource.api.express.models.request import com.squareup.moshi.Json -internal data class PairsRequestBody( +data class PairsRequestBody( @Json(name = "from") val from: List, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/Asset.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt similarity index 89% rename from features/swap/data/src/main/java/com/tangem/feature/swap/models/response/Asset.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt index 8d32b5d500..aaa590ea8b 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/Asset.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.models.response +package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt similarity index 94% rename from features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeDataResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt index 96ccbd9b14..3083bdfd0b 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.models.response +package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json import java.math.BigDecimal diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt similarity index 88% rename from features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeProvider.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index 432d4cff79..934d1a8497 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.models.response +package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt similarity index 79% rename from features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeQuoteResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt index 686c0e51f6..214b4ac2d5 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.models.response +package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json import java.math.BigDecimal diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeResultsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeResultsResponse.kt similarity index 92% rename from features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeResultsResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeResultsResponse.kt index 6b4db40e4a..c9b10f881e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/ExchangeResultsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeResultsResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.models.response +package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/SwapPair.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt similarity index 88% rename from features/swap/data/src/main/java/com/tangem/feature/swap/models/response/SwapPair.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt index 4ab2642970..bb62ceb94e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/models/response/SwapPair.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.models.response +package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json From be70d69092026c4331515cb95e4d969c3a109669 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Nov 2023 19:09:10 +0200 Subject: [PATCH 003/139] Updated on 2026-08-14 --- .../com/tangem/datasource/api/express/ExpressApi.kt | 10 +++------- .../api/express/models/request/AssetsRequestBody.kt | 2 +- .../api/express/models/request/PairsRequestBody.kt | 2 +- .../datasource/api/express/models/response/Asset.kt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt index 2ae9231c8f..8574c38d72 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt @@ -14,6 +14,7 @@ import java.math.BigDecimal /** * Interface of Tangem Express API (new swap mechanism) */ +@Suppress("LongParameterList") interface ExpressApi { // TODO move first three params to retrofit interceptor @@ -26,9 +27,7 @@ interface ExpressApi { ): ApiResponse> @POST("pairs") - suspend fun getPairs( - @Body body: PairsRequestBody, - ): ApiResponse> + suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse> @GET("providers") suspend fun getProviders(): ApiResponse> @@ -57,8 +56,5 @@ interface ExpressApi { ): ApiResponse @GET("exchange-results") - suspend fun getExchangeResults( - @Query("txId") txId: String, - ): ApiResponse - + suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt index e1f5629b05..f1d31bd24e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt @@ -4,4 +4,4 @@ import com.squareup.moshi.Json data class AssetsRequestBody( @Json(name = "filter") val filter: List?, -) +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt index 32c4b5f278..c22747d9cf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/PairsRequestBody.kt @@ -8,4 +8,4 @@ data class PairsRequestBody( @Json(name = "to") val to: List, -) +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt index aaa590ea8b..dc2fd581fe 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt @@ -25,5 +25,5 @@ data class Asset( val isActive: Boolean, @Json(name = "exchangeAvailable") - val exchangeAvailable: Boolean + val exchangeAvailable: Boolean, ) \ No newline at end of file From 1e0c7e02ff20f5efeb5853883b1d1ae8791390bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Nov 2023 12:38:27 +0200 Subject: [PATCH 004/139] Updated on 2026-08-14 --- .../network/auth/ExpressAuthProviderImpl.kt | 35 +++++++++++++++++++ .../datasource/api/express/ExpressApi.kt | 5 +-- .../com/tangem/datasource/di/NetworkModule.kt | 28 +++++++++++++++ .../tangem/datasource/utils/RequestHeader.kt | 7 ++++ .../tangem/lib/auth/ExpressAuthProvider.kt | 10 ++++++ .../lib/auth/ExpressSessionIdRefresher.kt | 6 ++++ 6 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt create mode 100644 libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt create mode 100644 libs/auth/src/main/java/com/tangem/lib/auth/ExpressSessionIdRefresher.kt diff --git a/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt b/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt new file mode 100644 index 0000000000..9d7e70badc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt @@ -0,0 +1,35 @@ +package com.tangem.tap.network.auth + +import com.tangem.common.extensions.toHexString +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.lib.auth.AuthProvider +import com.tangem.lib.auth.ExpressAuthProvider +import com.tangem.lib.auth.ExpressSessionIdGenerator +import com.tangem.tap.proxy.AppStateHolder +import java.util.UUID + +class ExpressAuthProviderImpl( + private val walletStateHolder: WalletsStateHolder, + private val userWalletsStore: UserWalletsStore, + private val appStateHolder: AppStateHolder +) : ExpressAuthProvider, ExpressSessionIdGenerator { + + private var uuid = UUID.randomUUID() + + override fun getApiKey(): String { + TODO() + } + + override fun getUserId(): String { + return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: "" + } + + override fun getSessionId(): String { + return uuid.toString() + } + + override fun generateNewSessionId() { + uuid = UUID.randomUUID() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt index 8574c38d72..9b5e7b2212 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt @@ -20,9 +20,6 @@ interface ExpressApi { // TODO move first three params to retrofit interceptor @POST("assets") suspend fun getAssets( - @Header("api-key") apiKey: String, - @Header("user-id") userId: String, - @Header("session-id") sessionId: String, @Body body: AssetsRequestBody, ): ApiResponse> @@ -55,6 +52,6 @@ interface ExpressApi { @Query("toAddress") toAddress: String, ): ApiResponse - @GET("exchange-results") + @GET("exchange-result") suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index f3e245e0d6..93f0528a18 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -3,12 +3,15 @@ package com.tangem.datasource.di import android.content.Context import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory +import com.tangem.datasource.api.express.ExpressApi import com.tangem.datasource.api.promotion.PromotionApi import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.utils.RequestHeader import com.tangem.datasource.utils.RequestHeader.* import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.addLoggers import com.tangem.lib.auth.AuthProvider +import com.tangem.lib.auth.ExpressAuthProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -24,6 +27,28 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) class NetworkModule { + @Provides + @Singleton + fun provideExpressApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + expressAuthProvider: ExpressAuthProvider, + ): ExpressApi { + + return Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) + .baseUrl(DEV_EXPRESS_BASE_URL) + .client( + OkHttpClient.Builder() + .addHeaders(Express(expressAuthProvider)) + .addLoggers(context) + .build(), + ) + .build() + .create(ExpressApi::class.java) + } + @Provides @Singleton fun provideTangemTechApi(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): TangemTechApi { @@ -74,6 +99,9 @@ class NetworkModule { } private companion object { + const val PROD_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]" + const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]" + const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/" const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/" diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt index 5e5067746f..b41f2c18f2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.utils import com.tangem.lib.auth.AuthProvider +import com.tangem.lib.auth.ExpressAuthProvider /** * Presentation of request header @@ -18,4 +19,10 @@ sealed class RequestHeader(vararg pairs: Pair String>) { "card_id" to { authProvider.getCardId() }, "card_public_key" to { authProvider.getCardPublicKey() }, ) + + class Express(expressAuthProvider: ExpressAuthProvider): RequestHeader( + "api-key" to { expressAuthProvider.getApiKey() }, + "user-id" to { expressAuthProvider.getUserId() }, + "session-id" to { expressAuthProvider.getSessionId() } + ) } \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt b/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt new file mode 100644 index 0000000000..4388254787 --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt @@ -0,0 +1,10 @@ +package com.tangem.lib.auth + +interface ExpressAuthProvider { + + fun getApiKey(): String + + fun getUserId(): String + + fun getSessionId(): String +} \ No newline at end of file diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/ExpressSessionIdRefresher.kt b/libs/auth/src/main/java/com/tangem/lib/auth/ExpressSessionIdRefresher.kt new file mode 100644 index 0000000000..0a8adb6718 --- /dev/null +++ b/libs/auth/src/main/java/com/tangem/lib/auth/ExpressSessionIdRefresher.kt @@ -0,0 +1,6 @@ +package com.tangem.lib.auth + +interface ExpressSessionIdGenerator { + + fun generateNewSessionId() +} \ No newline at end of file From 8439b5a6a6eac10db9c96b391147dbe408713e0b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Nov 2023 12:54:49 +0200 Subject: [PATCH 005/139] Updated on 2026-08-14 --- .../tap/network/auth/ExpressAuthProviderImpl.kt | 12 ++++-------- .../com/tangem/tap/network/auth/di/AuthModule.kt | 16 ++++++++++++++++ .../datasource/config/ConfigManagerImpl.kt | 1 + .../tangem/datasource/config/models/Config.kt | 1 + .../datasource/config/models/JsonModels.kt | 1 + .../ExpressSessionIdGenerator.kt} | 2 +- 6 files changed, 24 insertions(+), 9 deletions(-) rename libs/auth/src/main/java/com/tangem/lib/auth/{ExpressSessionIdRefresher.kt => sessionId/ExpressSessionIdGenerator.kt} (65%) diff --git a/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt b/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt index 9d7e70badc..53ed14fdbd 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt @@ -1,24 +1,20 @@ package com.tangem.tap.network.auth -import com.tangem.common.extensions.toHexString +import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.lib.auth.AuthProvider import com.tangem.lib.auth.ExpressAuthProvider -import com.tangem.lib.auth.ExpressSessionIdGenerator -import com.tangem.tap.proxy.AppStateHolder +import com.tangem.lib.auth.sessionId.ExpressSessionIdGenerator import java.util.UUID class ExpressAuthProviderImpl( - private val walletStateHolder: WalletsStateHolder, private val userWalletsStore: UserWalletsStore, - private val appStateHolder: AppStateHolder + private val configManager: ConfigManager ) : ExpressAuthProvider, ExpressSessionIdGenerator { private var uuid = UUID.randomUUID() override fun getApiKey(): String { - TODO() + return configManager.config.tangemExpressApiKey } override fun getUserId(): String { diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 6e9eabd1f1..7efa406601 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -1,7 +1,11 @@ package com.tangem.tap.network.auth.di +import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.lib.auth.AuthProvider +import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.tap.network.auth.AuthProviderImpl +import com.tangem.tap.network.auth.ExpressAuthProviderImpl import com.tangem.tap.proxy.AppStateHolder import dagger.Module import dagger.Provides @@ -18,4 +22,16 @@ class AuthModule { fun provideAuthProvider(appStateHolder: AppStateHolder): AuthProvider { return AuthProviderImpl(appStateHolder) } + + @Provides + @Singleton + fun provideExpressAuthProvider( + userWalletsStore: UserWalletsStore, + configManager: ConfigManager, + ): ExpressAuthProvider { + return ExpressAuthProviderImpl( + userWalletsStore = userWalletsStore, + configManager = configManager + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index 493d6b2e9e..20bd72d674 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -106,6 +106,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { swapReferrerAccount = configValues.swapReferrerAccount, walletConnectProjectId = configValues.walletConnectProjectId, tangemComAuthorization = configValues.tangemComAuthorization, + tangemExpressApiKey = configValues.tangemExpressApiKey ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt index 3d0bf5be7f..a4d08b27a3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt @@ -19,4 +19,5 @@ data class Config( val swapReferrerAccount: SwapReferrerAccount? = null, val walletConnectProjectId: String = "", val tangemComAuthorization: String? = null, + val tangemExpressApiKey: String = "" ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 83ffe5d09a..9a73d6ee83 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -40,6 +40,7 @@ class ConfigValueModel( val tangemComAuthorization: String?, val chiaFireAcademyApiKey: String?, val chiaTangemApiKey: String?, + val tangemExpressApiKey: String, ) data class AppsFlyer( diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/ExpressSessionIdRefresher.kt b/libs/auth/src/main/java/com/tangem/lib/auth/sessionId/ExpressSessionIdGenerator.kt similarity index 65% rename from libs/auth/src/main/java/com/tangem/lib/auth/ExpressSessionIdRefresher.kt rename to libs/auth/src/main/java/com/tangem/lib/auth/sessionId/ExpressSessionIdGenerator.kt index 0a8adb6718..be7270b4c7 100644 --- a/libs/auth/src/main/java/com/tangem/lib/auth/ExpressSessionIdRefresher.kt +++ b/libs/auth/src/main/java/com/tangem/lib/auth/sessionId/ExpressSessionIdGenerator.kt @@ -1,4 +1,4 @@ -package com.tangem.lib.auth +package com.tangem.lib.auth.sessionId interface ExpressSessionIdGenerator { From 33cb5a0bb027fd2f8a955df57b02e01b8cc6c984 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Nov 2023 14:27:16 +0200 Subject: [PATCH 006/139] Updated on 2026-08-14 --- .../main/java/com/tangem/datasource/api/express/ExpressApi.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt index 9b5e7b2212..790694e688 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt @@ -16,8 +16,7 @@ import java.math.BigDecimal */ @Suppress("LongParameterList") interface ExpressApi { - - // TODO move first three params to retrofit interceptor + @POST("assets") suspend fun getAssets( @Body body: AssetsRequestBody, From 90c6055403df10e76a701c1f73f34c55b762cae5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Nov 2023 16:47:04 +0200 Subject: [PATCH 007/139] Updated on 2026-08-14 --- .../datasource/api/express/models/response/SwapPair.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt index bb62ceb94e..ba04207f85 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt @@ -1,13 +1,14 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json +import com.tangem.datasource.api.express.models.request.LeastTokenInfo data class SwapPair( @Json(name = "from") - val from: String, + val from: LeastTokenInfo, @Json(name = "to") - val to: String, + val to: LeastTokenInfo, @Json(name = "providers") val providers: List, @@ -18,7 +19,7 @@ data class SwapPairProvider( @Json(name = "providerId") val providerId: Int, - @Json(name = "providerId") + @Json(name = "rateType") val rateType: RateType, ) From e7cd704beecc1771525ba85cd9148013d998cea5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Nov 2023 17:46:47 +0200 Subject: [PATCH 008/139] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index b84b1e1ab1..ade38b689e 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit b84b1e1ab11b0eb0f6ca6e28f551f9e2ca6809fe +Subproject commit ade38b689e5e390cf4105f9dfa641b4f6912dcd0 From 95f66c8798da8630b1aed76ccbb620901b88b7a3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Nov 2023 14:42:49 +0200 Subject: [PATCH 009/139] Updated on 2026-08-14 --- .../network/auth/ExpressAuthProviderImpl.kt | 2 +- .../tangem/tap/network/auth/di/AuthModule.kt | 2 +- .../{ExpressApi.kt => TangemExpressApi.kt} | 9 ++--- .../api/express/models/TangemExpressValues.kt | 5 +++ .../models/request/AssetsRequestBody.kt | 3 +- .../datasource/config/ConfigManagerImpl.kt | 2 +- .../tangem/datasource/config/models/Config.kt | 2 +- ...insStoreModule.kt => AssetsStoreModule.kt} | 10 +++--- .../com/tangem/datasource/di/NetworkModule.kt | 8 ++--- .../datasource/local/token/AssetsStore.kt | 11 +++++++ ...ketCoinsStore.kt => DefaultAssetsStore.kt} | 12 +++---- .../local/token/UserMarketCoinsStore.kt | 11 ------- .../tangem/datasource/utils/RequestHeader.kt | 4 +-- .../tangem/data/tokens/di/TokensDataModule.kt | 15 +++++---- .../repository/DefaultCurrenciesRepository.kt | 33 ++++++++++++++----- .../DefaultMarketCryptoCurrencyRepository.kt | 20 +++++++---- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 2 +- .../MarketCryptoCurrencyRepository.kt | 2 +- 18 files changed, 89 insertions(+), 64 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/api/express/{ExpressApi.kt => TangemExpressApi.kt} (92%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/express/models/TangemExpressValues.kt rename core/datasource/src/main/java/com/tangem/datasource/di/{MarketCoinsStoreModule.kt => AssetsStoreModule.kt} (51%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt rename core/datasource/src/main/java/com/tangem/datasource/local/token/{DefaultUserMarketCoinsStore.kt => DefaultAssetsStore.kt} (64%) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt diff --git a/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt b/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt index 53ed14fdbd..2a7f65eee3 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt @@ -8,7 +8,7 @@ import java.util.UUID class ExpressAuthProviderImpl( private val userWalletsStore: UserWalletsStore, - private val configManager: ConfigManager + private val configManager: ConfigManager, ) : ExpressAuthProvider, ExpressSessionIdGenerator { private var uuid = UUID.randomUUID() diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 7efa406601..3ae06e158e 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -31,7 +31,7 @@ class AuthModule { ): ExpressAuthProvider { return ExpressAuthProviderImpl( userWalletsStore = userWalletsStore, - configManager = configManager + configManager = configManager, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt similarity index 92% rename from core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 790694e688..627e70bdd0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -6,7 +6,6 @@ import com.tangem.datasource.api.express.models.request.PairsRequestBody import com.tangem.datasource.api.express.models.response.* import retrofit2.http.Body import retrofit2.http.GET -import retrofit2.http.Header import retrofit2.http.POST import retrofit2.http.Query import java.math.BigDecimal @@ -15,12 +14,10 @@ import java.math.BigDecimal * Interface of Tangem Express API (new swap mechanism) */ @Suppress("LongParameterList") -interface ExpressApi { - +interface TangemExpressApi { + @POST("assets") - suspend fun getAssets( - @Body body: AssetsRequestBody, - ): ApiResponse> + suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse> @POST("pairs") suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse> diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/TangemExpressValues.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/TangemExpressValues.kt new file mode 100644 index 0000000000..984184791c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/TangemExpressValues.kt @@ -0,0 +1,5 @@ +package com.tangem.datasource.api.express.models + +object TangemExpressValues { + const val EMPTY_CONTRACT_ADDRESS_VALUE = "0" +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt index f1d31bd24e..ffe4a0a5b0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt @@ -3,5 +3,6 @@ package com.tangem.datasource.api.express.models.request import com.squareup.moshi.Json data class AssetsRequestBody( - @Json(name = "filter") val filter: List?, + @Json(name = "tokensList") val tokensList: List?, + @Json(name = "onlyActive") val onlyActive: Boolean, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index 20bd72d674..3fced2471f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -106,7 +106,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { swapReferrerAccount = configValues.swapReferrerAccount, walletConnectProjectId = configValues.walletConnectProjectId, tangemComAuthorization = configValues.tangemComAuthorization, - tangemExpressApiKey = configValues.tangemExpressApiKey + tangemExpressApiKey = configValues.tangemExpressApiKey, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt index a4d08b27a3..00596c5621 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt @@ -19,5 +19,5 @@ data class Config( val swapReferrerAccount: SwapReferrerAccount? = null, val walletConnectProjectId: String = "", val tangemComAuthorization: String? = null, - val tangemExpressApiKey: String = "" + val tangemExpressApiKey: String = "", ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt similarity index 51% rename from core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt rename to core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt index bd2098a670..43b8e93f5c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.token.DefaultUserMarketCoinsStore -import com.tangem.datasource.local.token.UserMarketCoinsStore +import com.tangem.datasource.local.token.DefaultAssetsStore +import com.tangem.datasource.local.token.AssetsStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,11 +11,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object MarketCoinsStoreModule { +internal object AssetsStoreModule { @Provides @Singleton - fun provideUserMarketCoinsStore(): UserMarketCoinsStore { - return DefaultUserMarketCoinsStore(dataStore = RuntimeDataStore()) + fun provideAssetsStore(): AssetsStore { + return DefaultAssetsStore(dataStore = RuntimeDataStore()) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 93f0528a18..7d1f732da5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -3,10 +3,9 @@ package com.tangem.datasource.di import android.content.Context import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory -import com.tangem.datasource.api.express.ExpressApi +import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.promotion.PromotionApi import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.utils.RequestHeader import com.tangem.datasource.utils.RequestHeader.* import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.addLoggers @@ -33,8 +32,7 @@ class NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, expressAuthProvider: ExpressAuthProvider, - ): ExpressApi { - + ): TangemExpressApi { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) @@ -46,7 +44,7 @@ class NetworkModule { .build(), ) .build() - .create(ExpressApi::class.java) + .create(TangemExpressApi::class.java) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt new file mode 100644 index 0000000000..11d6101843 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.express.models.response.Asset +import com.tangem.domain.wallets.models.UserWalletId + +interface AssetsStore { + + suspend fun getSyncOrNull(userWalletId: UserWalletId): List? + + suspend fun store(userWalletId: UserWalletId, item: List) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt similarity index 64% rename from core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt index c08a627df9..9177ec70b7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt @@ -1,18 +1,18 @@ package com.tangem.datasource.local.token -import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.datasource.api.express.models.response.Asset import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.domain.wallets.models.UserWalletId -internal class DefaultUserMarketCoinsStore( - private val dataStore: StringKeyDataStore, -) : UserMarketCoinsStore { +internal class DefaultAssetsStore( + private val dataStore: StringKeyDataStore>, +) : AssetsStore { - override suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? { + override suspend fun getSyncOrNull(userWalletId: UserWalletId): List? { return dataStore.getSyncOrNull(userWalletId.stringValue) } - override suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) { + override suspend fun store(userWalletId: UserWalletId, item: List) { dataStore.store(userWalletId.stringValue, item) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt deleted file mode 100644 index 10895699ac..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.datasource.local.token - -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.wallets.models.UserWalletId - -interface UserMarketCoinsStore { - - suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? - - suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt index b41f2c18f2..6a5fde6a53 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt @@ -20,9 +20,9 @@ sealed class RequestHeader(vararg pairs: Pair String>) { "card_public_key" to { authProvider.getCardPublicKey() }, ) - class Express(expressAuthProvider: ExpressAuthProvider): RequestHeader( + class Express(expressAuthProvider: ExpressAuthProvider) : RequestHeader( "api-key" to { expressAuthProvider.getApiKey() }, "user-id" to { expressAuthProvider.getUserId() }, - "session-id" to { expressAuthProvider.getSessionId() } + "session-id" to { expressAuthProvider.getSessionId() }, ) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index f8e33206dd..b683931af9 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -5,11 +5,12 @@ import com.tangem.data.tokens.repository.DefaultCurrenciesRepository import com.tangem.data.tokens.repository.DefaultMarketCryptoCurrencyRepository import com.tangem.data.tokens.repository.DefaultNetworksRepository import com.tangem.data.tokens.repository.DefaultQuotesRepository +import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.quote.QuotesStore -import com.tangem.datasource.local.token.UserMarketCoinsStore +import com.tangem.datasource.local.token.AssetsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -32,17 +33,19 @@ internal object TokensDataModule { @Singleton fun provideCurrenciesRepository( tangemTechApi: TangemTechApi, + tangemExpressApi: TangemExpressApi, userTokensStore: UserTokensStore, userWalletsStore: UserWalletsStore, - userMarketCoinsStore: UserMarketCoinsStore, + assetsStore: AssetsStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): CurrenciesRepository { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, + tangemExpressApi = tangemExpressApi, userTokensStore = userTokensStore, userWalletsStore = userWalletsStore, - userMarketCoinsStore = userMarketCoinsStore, + assetsStore = assetsStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, ) @@ -88,9 +91,7 @@ internal object TokensDataModule { @Provides @Singleton - fun provideDefaultMarketCoinsRepository( - userMarketCoinsStore: UserMarketCoinsStore, - ): MarketCryptoCurrencyRepository { - return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore) + fun provideDefaultMarketCoinsRepository(assetsStore: AssetsStore): MarketCryptoCurrencyRepository { + return DefaultMarketCryptoCurrencyRepository(assetsStore) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 3f623010d5..7caa36ac26 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -5,9 +5,14 @@ import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.* import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE +import com.tangem.datasource.api.express.models.request.AssetsRequestBody +import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.token.UserMarketCoinsStore +import com.tangem.datasource.local.token.AssetsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.extensions.toCoinId @@ -27,12 +32,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -@Suppress("LargeClass") +@Suppress("LargeClass", "LongParameterList") internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, + private val tangemExpressApi: TangemExpressApi, private val userTokensStore: UserTokensStore, private val userWalletsStore: UserWalletsStore, - private val userMarketCoinsStore: UserMarketCoinsStore, + private val assetsStore: AssetsStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : CurrenciesRepository { @@ -341,14 +347,25 @@ internal class DefaultCurrenciesRepository( userTokens: UserTokensResponse, ) { try { - val networkIds = userTokens.tokens + val tokensList = userTokens.tokens .distinctBy { it.networkId } - .joinToString(separator = ",") { it.networkId } - val response = tangemTechApi.getCoins(networkIds = networkIds, exchangeable = true) + .map { + LeastTokenInfo( + contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, + network = it.networkId, + ) + } - userMarketCoinsStore.store(userWalletId, response) + val response = tangemExpressApi.getAssets( + AssetsRequestBody( + tokensList = tokensList, + onlyActive = true, + ), + ) + + assetsStore.store(userWalletId, response.getOrThrow()) } catch (e: Throwable) { - Timber.e(e, "Unable to fetch user market coins for: ${userWalletId.stringValue}") + Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt index 441f3f97d2..b2bc8bf0a2 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -1,22 +1,28 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.common.Blockchain -import com.tangem.datasource.local.token.UserMarketCoinsStore +import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE +import com.tangem.datasource.local.token.AssetsStore import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.wallets.models.UserWalletId class DefaultMarketCryptoCurrencyRepository( - private val userMarketCoinsStore: UserMarketCoinsStore, + private val assetsStore: AssetsStore, ) : MarketCryptoCurrencyRepository { - override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean { + override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { + val cryptoCurrencyId = cryptoCurrency.id val blockchain = Blockchain.fromId(cryptoCurrencyId.rawNetworkId) val apiNetworkId = blockchain.toNetworkId() - return userMarketCoinsStore.getSyncOrNull(userWalletId)?.coins - ?.firstOrNull { it.id == cryptoCurrencyId.rawCurrencyId } - ?.networks - ?.firstOrNull { it.networkId == apiNetworkId }?.exchangeable ?: false + val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE + + return assetsStore.getSyncOrNull(userWalletId)?.find { + it.network == apiNetworkId && + it.token == cryptoCurrencyId.rawCurrencyId && + it.contractAddress == contractAddress && + it.isActive + }?.exchangeAvailable ?: false } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 718b412716..a6c9f21f01 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -84,7 +84,7 @@ class GetCryptoCurrencyActionsUseCase( activeList.add(TokenActionsState.ActionState.Receive(true)) // swap - if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency.id)) { + if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency)) { activeList.add(TokenActionsState.ActionState.Swap(true)) } else { disabledList.add(TokenActionsState.ActionState.Swap(false)) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt index 0186226918..0e0d223a53 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt @@ -8,5 +8,5 @@ import com.tangem.domain.wallets.models.UserWalletId */ interface MarketCryptoCurrencyRepository { - suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean + suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean } \ No newline at end of file From cfdcf4da4162db308ffc865ee7864cfff20c6f65 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Nov 2023 14:22:43 +0200 Subject: [PATCH 010/139] Updated on 2026-08-14 --- .../api/express/models/response/SwapPair.kt | 4 +- .../DefaultMarketCryptoCurrencyRepository.kt | 9 +-- .../data/tokens/utils/NetworkOperations.kt | 2 + .../com/tangem/domain/tokens/model/Network.kt | 2 + .../tangem/feature/swap/SwapRepositoryImpl.kt | 60 ++++++++++++++++-- .../converters/LeastTokenInfoConverter.kt | 17 ++++++ .../swap/converters/SwapPairInfoConverter.kt | 43 +++++++++++++ .../tangem/feature/swap/di/SwapDataModule.kt | 3 + .../feature/swap/domain/SwapInteractor.kt | 9 ++- .../feature/swap/domain/SwapInteractorImpl.kt | 61 +++++++++++++++++++ .../feature/swap/domain/SwapRepository.kt | 6 +- .../swap/domain/di/SwapDomainModule.kt | 10 +++ .../domain/models/domain/LeastTokenInfo.kt | 6 ++ .../domain/models/domain/SwapPairLeast.kt | 50 +++++++++++++++ .../swap/viewmodels/SwapProcessDataState.kt | 7 +++ .../feature/swap/viewmodels/SwapViewModel.kt | 9 +++ 16 files changed, 278 insertions(+), 20 deletions(-) create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt index ba04207f85..2d8b0b7cf3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt @@ -19,8 +19,8 @@ data class SwapPairProvider( @Json(name = "providerId") val providerId: Int, - @Json(name = "rateType") - val rateType: RateType, + @Json(name = "rateTypes") + val rateTypes: List, ) enum class RateType { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt index b2bc8bf0a2..9e8d9a059d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -1,9 +1,7 @@ package com.tangem.data.tokens.repository -import com.tangem.blockchain.common.Blockchain import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE import com.tangem.datasource.local.token.AssetsStore -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.wallets.models.UserWalletId @@ -13,14 +11,11 @@ class DefaultMarketCryptoCurrencyRepository( ) : MarketCryptoCurrencyRepository { override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { - val cryptoCurrencyId = cryptoCurrency.id - val blockchain = Blockchain.fromId(cryptoCurrencyId.rawNetworkId) - val apiNetworkId = blockchain.toNetworkId() val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE return assetsStore.getSyncOrNull(userWalletId)?.find { - it.network == apiNetworkId && - it.token == cryptoCurrencyId.rawCurrencyId && + it.network == cryptoCurrency.network.backendId && + it.token == cryptoCurrency.id.rawCurrencyId && it.contractAddress == contractAddress && it.isActive }?.exchangeAvailable ?: false diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt index 5e87555133..f5a1bf13df 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt @@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.DerivationStyleProvider +import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.Network import timber.log.Timber @@ -21,6 +22,7 @@ internal fun getNetwork( return Network( id = Network.ID(blockchain.id), + backendId = blockchain.toNetworkId(), name = blockchain.fullName, isTestnet = blockchain.isTestnet(), derivationPath = getNetworkDerivationPath(blockchain, extraDerivationPath, derivationStyleProvider), diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt index 2bc545ba77..c57d5beee8 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt @@ -11,6 +11,7 @@ import kotlinx.parcelize.Parcelize * (e.g., ERC20, BEP20). * * @property id The unique identifier of the network. + * @property backendId The name of this network in the Tangem backend. * @property name The human-readable name of the network, such as "Ethereum" or "Bitcoin". * @property derivationPath The path used to derive keys for this network. * @property isTestnet Indicates whether the network is a test network or a main network. @@ -19,6 +20,7 @@ import kotlinx.parcelize.Parcelize @Parcelize data class Network( val id: ID, + val backendId: String, val name: String, val derivationPath: DerivationPath, val isTestnet: Boolean, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index e5394f3013..e5f16ea2ac 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -6,6 +6,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.extensions.Result import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.request.PairsRequestBody import com.tangem.datasource.api.oneinch.OneInchApi import com.tangem.datasource.api.oneinch.OneInchApiFactory import com.tangem.datasource.api.oneinch.OneInchErrorsHandler @@ -19,23 +22,23 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.swap.converters.QuotesConverter -import com.tangem.feature.swap.converters.SwapConverter -import com.tangem.feature.swap.converters.TokensConverter +import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.SwapRepository import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel -import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.feature.swap.domain.models.domain.QuoteModel -import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.mapErrors import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.async import kotlinx.coroutines.withContext import java.math.BigDecimal import javax.inject.Inject import com.tangem.blockchain.common.Token as SdkToken +import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo +@Suppress("LongParameterList") internal class SwapRepositoryImpl @Inject constructor( private val tangemTechApi: TangemTechApi, + private val tangemExpressApi: TangemExpressApi, private val oneInchApiFactory: OneInchApiFactory, private val oneInchErrorsHandler: OneInchErrorsHandler, private val coroutineDispatcher: CoroutineDispatcherProvider, @@ -46,6 +49,51 @@ internal class SwapRepositoryImpl @Inject constructor( private val tokensConverter = TokensConverter() private val quotesConverter = QuotesConverter() private val swapConverter = SwapConverter() + private val leastTokenInfoConverter = LeastTokenInfoConverter() + private val swapPairInfoConverter = SwapPairInfoConverter() + + override suspend fun getPairs( + initialCurrency: LeastTokenInfo, + currencyList: List, + ): List { + return withContext(coroutineDispatcher.io) { + val initial = NetworkLeastTokenInfo( + contractAddress = initialCurrency.contractAddress, + network = initialCurrency.network, + ) + val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) } + + val pairs = async { + getPairsInternal( + from = arrayListOf(initial), + to = currenciesList, + ) + } + + val reversedPairs = async { + getPairsInternal( + from = currenciesList, + to = arrayListOf(initial), + ) + } + + pairs.await() + reversedPairs.await() + } + } + + private suspend fun getPairsInternal( + from: List, + to: List, + ): List { + return tangemExpressApi.getPairs( + PairsRequestBody( + from = from, + to = to, + ), + ) + .getOrThrow() + .map { swapPairInfoConverter.convert(it) } + } override suspend fun getRates(currencyId: String, tokenIds: List): Map { // workaround cause backend do not return arbitrum and optimism rates diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt new file mode 100644 index 0000000000..4ee7532cc7 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.swap.converters + +import com.tangem.blockchain.common.Blockchain +import com.tangem.datasource.api.express.models.request.LeastTokenInfo +import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.converter.Converter + +class LeastTokenInfoConverter : Converter { + + override fun convert(value: CryptoCurrency): LeastTokenInfo { + return LeastTokenInfo( + contractAddress = (value as? CryptoCurrency.Token)?.contractAddress ?: "0", + network = Blockchain.fromId(value.id.rawNetworkId).toNetworkId(), + ) + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt new file mode 100644 index 0000000000..627a7a8fb4 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -0,0 +1,43 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.express.models.response.RateType +import com.tangem.datasource.api.express.models.response.SwapPair +import com.tangem.datasource.api.express.models.response.SwapPairProvider +import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo +import com.tangem.feature.swap.domain.models.domain.SwapPairLeast as SwapPairDomain +import com.tangem.feature.swap.domain.models.domain.SwapPairProvider as SwapPairProviderDomain +import com.tangem.feature.swap.domain.models.domain.RateType as RateTypeDomain +import com.tangem.utils.converter.Converter + +class SwapPairInfoConverter : Converter { + + override fun convert(value: SwapPair): SwapPairDomain { + return SwapPairDomain( + from = LeastTokenInfo( + contractAddress = value.from.contractAddress, + network = value.from.network, + ), + to = LeastTokenInfo( + contractAddress = value.to.contractAddress, + network = value.to.network, + ), + providers = value.providers.map { + convertProvider(it) + }, + ) + } + + private fun convertProvider(swapPairProvider: SwapPairProvider): SwapPairProviderDomain { + return SwapPairProviderDomain( + providerId = swapPairProvider.providerId, + rateTypes = swapPairProvider.rateTypes.map { convertRateType(it) }, + ) + } + + private fun convertRateType(rateType: RateType): RateTypeDomain { + return when (rateType) { + RateType.FIXED -> RateTypeDomain.FIXED + RateType.FLOAT -> RateTypeDomain.FLOAT + } + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 81931ca111..89506db425 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.di +import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.oneinch.OneInchApiFactory import com.tangem.datasource.api.oneinch.OneInchErrorsHandler import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -22,6 +23,7 @@ class SwapDataModule { @Singleton fun provideSwapRepository( tangemTechApi: TangemTechApi, + tangemExpressApi: TangemExpressApi, oneInchApiFactory: OneInchApiFactory, oneInchErrorsHandler: OneInchErrorsHandler, coroutineDispatcher: CoroutineDispatcherProvider, @@ -30,6 +32,7 @@ class SwapDataModule { ): SwapRepository { return SwapRepositoryImpl( tangemTechApi = tangemTechApi, + tangemExpressApi = tangemExpressApi, oneInchApiFactory = oneInchApiFactory, oneInchErrorsHandler = oneInchErrorsHandler, coroutineDispatcher = coroutineDispatcher, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 3cae5f1d7e..bfa09c1408 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -1,13 +1,17 @@ package com.tangem.feature.swap.domain +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.feature.swap.domain.models.domain.PermissionOptions +import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* interface SwapInteractor { + suspend fun getPairs(currency: Currency): List + + suspend fun getPairs(initialCurrency: LeastTokenInfo, currenciesList: List): List + fun initDerivationPathAndNetwork(derivationPath: String?, network: Network?) /** @@ -17,6 +21,7 @@ interface SwapInteractor { * @return [TokensDataState] that contains info about all available to swap tokens for networkId * and preselected tokens which initially select to swap */ + @Deprecated("method is used in the old swap mechanism") suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState /** diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 50e7999aeb..d3dc616711 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,5 +1,7 @@ package com.tangem.feature.swap.domain +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository @@ -34,6 +36,7 @@ internal class SwapInteractorImpl @Inject constructor( private val networksRepository: NetworksRepository, private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, ) : SwapInteractor { private val swapCurrencyConverter = SwapCurrencyConverter() @@ -41,6 +44,64 @@ internal class SwapInteractorImpl @Inject constructor( private var derivationPath: String? = null private var network: Network? = null + override suspend fun getPairs(currency: Currency): List { + val currencies = getSelectedWalletSyncUseCase().fold( + ifLeft = { emptyList() }, + ifRight = { selectedWallet -> + getCryptoCurrenciesUseCase(selectedWallet.walletId).fold( + ifLeft = { emptyList() }, + ifRight = { it }, + ) + }, + ) + + val pairs = getPairs( + initialCurrency = LeastTokenInfo( + contractAddress = (currency as? Currency.NonNativeToken)?.contractAddress ?: "0", + network = currency.networkId, + ), + currenciesList = currencies, + ) + + return createCryptoCurrencyPairs(pairs, currencies) + } + + private fun createCryptoCurrencyPairs( + swapPairLeasts: List, + cryptoCurrenciesList: List, + ): List { + return swapPairLeasts.mapNotNull { + val from = findCryptoCurrencyByLeastInfo(it.from, cryptoCurrenciesList) + val to = findCryptoCurrencyByLeastInfo(it.to, cryptoCurrenciesList) + if (from != null && to != null) { + SwapPair( + from = from, + to = to, + providers = it.providers, + ) + } else { + null + } + } + } + + private fun findCryptoCurrencyByLeastInfo( + leastTokenInfo: LeastTokenInfo, + cryptoCurrenciesList: List, + ): CryptoCurrency? { + return cryptoCurrenciesList.find { + it.network.backendId == leastTokenInfo.network && + (it as? CryptoCurrency.Token)?.contractAddress ?: "0" == leastTokenInfo.contractAddress + } + } + + override suspend fun getPairs( + initialCurrency: LeastTokenInfo, + currenciesList: List, + ): List { + return repository.getPairs(initialCurrency, currenciesList) + } + override fun initDerivationPathAndNetwork(derivationPath: String?, network: Network?) { this.derivationPath = derivationPath this.network = network diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index b303325c8c..3b64564a3b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -5,13 +5,13 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel -import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.feature.swap.domain.models.domain.QuoteModel -import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.* import java.math.BigDecimal interface SwapRepository { + suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List): List + suspend fun getRates(currencyId: String, tokenIds: List): Map suspend fun getExchangeableTokens(networkId: String): List diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 7a94dfb303..bba56b76e7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.domain.di +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.wallets.legacy.WalletsStateHolder @@ -30,6 +31,7 @@ class SwapDomainModule { networksRepository: NetworksRepository, walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + @SwapScope getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -41,6 +43,7 @@ class SwapDomainModule { networksRepository = networksRepository, walletFeatureToggles = walletFeatureToggles, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + getCryptoCurrenciesUseCase = getCryptoCurrenciesUseCase, ) } @@ -58,6 +61,13 @@ class SwapDomainModule { fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletSyncUseCase { return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder) } + + @SwapScope + @Provides + @Singleton + fun providesGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { + return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository) + } } @Qualifier diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt new file mode 100644 index 0000000000..b9ed4f4af3 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt @@ -0,0 +1,6 @@ +package com.tangem.feature.swap.domain.models.domain + +data class LeastTokenInfo( + val contractAddress: String, + val network: String, +) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt new file mode 100644 index 0000000000..704a27dc1b --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.swap.domain.models.domain + +import com.tangem.domain.tokens.model.CryptoCurrency + +/** + * Domain layer representation of SwapPair data network model. + * + * @property from Short information about the token we want to change + * @property to Short information about the token we want to exchange for + * @property providers Exchange providers + */ +data class SwapPairLeast( + val from: LeastTokenInfo, + val to: LeastTokenInfo, + val providers: List, +) + +/** + * Enriched model of swap pair data. Contains full CryptoCurrency models instead of least info. + * + * @property from CryptoCurrency we want to change + * @property to CryptoCurrency we want to exchange for + * @property providers Exchange providers + */ +data class SwapPair( + val from: CryptoCurrency, + val to: CryptoCurrency, + val providers: List, +) + +/** + * Provider that could swap given cryptocurrencies + * + * @property providerId provider id + * @property rateTypes supported rate types + */ +data class SwapPairProvider( + val providerId: Int, + val rateTypes: List, +) + +/** + * Rate type. + * + * Current implementation contains only float type, fixed will be supported later. + */ +enum class RateType { + FLOAT, + FIXED, +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index 5b4b806568..65abcf814c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -1,14 +1,21 @@ package com.tangem.feature.swap.viewmodels +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData import com.tangem.feature.swap.domain.models.ui.SwapStateData import com.tangem.feature.swap.domain.models.ui.TxFee data class SwapProcessDataState( + // Initial network id val networkId: String, + @Deprecated("used in old swap mechanism") val fromCurrency: Currency? = null, + @Deprecated("used in old swap mechanism") val toCurrency: Currency? = null, + val fromCryptoCurrency: CryptoCurrency? = null, + val toCryptoCurrency: CryptoCurrency? = null, + // Amount from input val amount: String? = null, val approveDataModel: RequestApproveStateData? = null, val swapDataModel: SwapStateData? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 7860b28db9..53659922c3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -137,6 +137,15 @@ internal class SwapViewModel @Inject constructor( } private fun initTokens(currency: Currency) { + // new flow + viewModelScope.launch(dispatchers.main) { + runCatching(dispatchers.io) { + val pairs = swapInteractor.getPairs(currency) + // TODO + } + } + + // old flow viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { swapInteractor.initTokensToSwap(currency) From ca03d3d667df500f1a0b99203e1d79f83a7b8e7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Nov 2023 15:24:57 +0300 Subject: [PATCH 011/139] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index ade38b689e..f5a90466d2 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit ade38b689e5e390cf4105f9dfa641b4f6912dcd0 +Subproject commit f5a90466d2245ca5cfde59b49dc7fa82a6b7cbc2 From ddab2eb560dbaf116c1544077d4e1b14472f7927 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Nov 2023 15:26:14 +0300 Subject: [PATCH 012/139] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 4bca06433a..c9c2686539 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -127,6 +127,7 @@ internal class SwapViewModel @Inject constructor( ) } + @Suppress("UnusedPrivateMember") private fun initTokens(currency: Currency) { // new flow viewModelScope.launch(dispatchers.main) { From 3aa098dcd458ed92784fddf9f703ad61ecebd821 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Nov 2023 15:30:46 +0300 Subject: [PATCH 013/139] Updated on 2026-08-14 --- .../swap/converters/TokensDataConverter.kt | 20 +- .../swap/models/SwapSelectTokenStateHolder.kt | 32 ++- .../tangem/feature/swap/ui/StateBuilder.kt | 17 +- .../feature/swap/ui/SwapSelectTokenScreen.kt | 236 +++++++----------- 4 files changed, 141 insertions(+), 164 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index cee76edbb1..ef3b9c7ac3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.converters import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.domain.NetworkInfo import com.tangem.feature.swap.domain.models.ui.FoundTokensState @@ -8,7 +9,8 @@ import com.tangem.feature.swap.domain.models.ui.TokenWithBalance import com.tangem.feature.swap.models.Network import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData -import com.tangem.feature.swap.models.TokenToSelect +import com.tangem.feature.swap.models.TokenToSelectState +import kotlinx.collections.immutable.toImmutableList class TokensDataConverter( private val onSearchEntered: (String) -> Unit, @@ -18,21 +20,27 @@ class TokensDataConverter( fun convertWithNetwork(value: FoundTokensState, network: NetworkInfo): SwapSelectTokenStateHolder { return SwapSelectTokenStateHolder( - addedTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) }, - otherTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) }, + availableTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), + unavailableTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, network = Network(network.name, network.blockchainId), ) } - private fun tokenWithBalanceToTokenToSelect(tokenWithBalance: TokenWithBalance): TokenToSelect { - return TokenToSelect( + private fun tokenWithBalanceToTokenToSelect(tokenWithBalance: TokenWithBalance): TokenToSelectState.TokenToSelect { + return TokenToSelectState.TokenToSelect( id = tokenWithBalance.token.id, name = tokenWithBalance.token.name, symbol = tokenWithBalance.token.symbol, - iconUrl = tokenWithBalance.token.logoUrl, isNative = tokenWithBalance.token is Currency.NativeToken, + // todo replace converting + tokenIcon = TokenIconState.CoinIcon( + url = "", + fallbackResId = 0, + isGrayscale = false, + showCustomBadge = false, + ), addedTokenBalanceData = TokenBalanceData( amount = tokenWithBalance.tokenBalanceData?.amount, amountEquivalent = tokenWithBalance.tokenBalanceData?.amountEquivalent, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 2e48e108cc..382da8e588 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -1,28 +1,36 @@ package com.tangem.feature.swap.models +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import kotlinx.collections.immutable.ImmutableList + data class SwapSelectTokenStateHolder( - val addedTokens: List, - val otherTokens: List, + val availableTokens: ImmutableList, + val unavailableTokens: ImmutableList, val network: Network, val onSearchEntered: (String) -> Unit, val onTokenSelected: (String) -> Unit, ) -data class TokenToSelect( - val id: String, - val name: String, - val symbol: String, - val iconUrl: String, - val isNative: Boolean, - val available: Boolean = true, - val addedTokenBalanceData: TokenBalanceData? = null, -) - data class Network( val name: String, val blockchainId: String, ) +sealed class TokenToSelectState { + + data class Title(val title: String) : TokenToSelectState() + + data class TokenToSelect( + val id: String, + val name: String, + val symbol: String, + val isNative: Boolean, + val tokenIcon: TokenIconState, + val available: Boolean = true, + val addedTokenBalanceData: TokenBalanceData? = null, + ) : TokenToSelectState() +} + data class TokenBalanceData( val amount: String?, val amountEquivalent: String?, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 75b0e2b05f..af9fc07c61 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -274,11 +274,18 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: isBalanceHidden = isBalanceHidden, ) val selectTokenState = uiState.selectTokenState?.copy( - addedTokens = uiState.selectTokenState.addedTokens.map { - it.copy( - addedTokenBalanceData = it.addedTokenBalanceData?.copy(isBalanceHidden = isBalanceHidden), - ) - }, + availableTokens = uiState.selectTokenState.availableTokens.map { + when (it) { + is TokenToSelectState.TokenToSelect -> { + it.copy( + addedTokenBalanceData = it.addedTokenBalanceData?.copy(isBalanceHidden = isBalanceHidden), + ) + } + is TokenToSelectState.Title -> { + it + } + } + }.toImmutableList(), ) return uiState.copy( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 566447924f..1faa262bb3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -1,45 +1,31 @@ package com.tangem.feature.swap.ui -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.Divider import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest import com.tangem.common.Strings -import com.tangem.core.ui.components.CircleShimmer -import com.tangem.core.ui.components.CurrencyPlaceholderIcon -import com.tangem.core.ui.components.SpacerW2 +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.ExpandableSearchView +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.feature.swap.models.Network -import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.TokenBalanceData -import com.tangem.feature.swap.models.TokenToSelect +import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.R -import kotlinx.coroutines.launch +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList @Composable fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) { @@ -64,7 +50,6 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) ) } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) { val screenBackgroundColor = TangemTheme.colors.background.secondary @@ -74,44 +59,45 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { - if (state.addedTokens.isNotEmpty()) { - stickyHeader { Header(title = R.string.swapping_token_list_your_tokens) } - } + item { SpacerH8() } - itemsIndexed(items = state.addedTokens) { index, item -> - TokenItem( - token = item, - network = state.network, - screenBackgroundColor = screenBackgroundColor, - onTokenClick = { - state - .onTokenSelected(item.id) - }, - ) + tokensToSelectItems(state.availableTokens, state.onTokenSelected) - if (index != state.addedTokens.lastIndex) { - Divider( - color = TangemTheme.colors.stroke.primary, - startIndent = TangemTheme.dimens.spacing54, + item { SpacerH12() } + + tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) + + item { SpacerH12() } + } +} + +private fun LazyListScope.tokensToSelectItems( + items: ImmutableList, + onTokenClick: (String) -> Unit, +) { + itemsIndexed(items = items) { index, item -> + when (item) { + is TokenToSelectState.Title -> { + TitleHeader( + item = item, + modifier = Modifier.roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + ), ) } - } - - if (state.otherTokens.isNotEmpty()) { - stickyHeader { Header(title = R.string.swapping_token_list_other_tokens) } - } - - itemsIndexed(items = state.otherTokens) { index, item -> - TokenItem( - token = item, - network = state.network, - screenBackgroundColor = screenBackgroundColor, - onTokenClick = { state.onTokenSelected(item.id) }, - ) - if (index != state.otherTokens.lastIndex) { - Divider( - color = TangemTheme.colors.stroke.primary, - startIndent = TangemTheme.dimens.spacing54, + is TokenToSelectState.TokenToSelect -> { + TokenItem( + token = item, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + ) + .background(TangemTheme.colors.background.action), + onTokenClick = { + onTokenClick(item.id) + }, ) } } @@ -119,27 +105,35 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = } @Composable -private fun Header(@StringRes title: Int) { - Text( - text = stringResource(id = title).uppercase(), - style = TangemTheme.typography.overline, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier +private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Modifier) { + Box( + modifier = modifier .fillMaxWidth() - .padding( - vertical = TangemTheme.dimens.spacing6, - horizontal = TangemTheme.dimens.spacing16, - ), - textAlign = TextAlign.Start, - ) + .background(TangemTheme.colors.background.action), + ) { + Text( + text = item.title, + style = TangemTheme.typography.overline, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing16, + ), + ) + } } @Suppress("LongMethod") @Composable -private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundColor: Color, onTokenClick: () -> Unit) { +private fun TokenItem( + token: TokenToSelectState.TokenToSelect, + onTokenClick: () -> Unit, + modifier: Modifier = Modifier, +) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() + .height(TangemTheme.dimens.size72) .clickable(onClick = onTokenClick) .padding( vertical = TangemTheme.dimens.spacing14, @@ -148,12 +142,15 @@ private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundCo verticalAlignment = Alignment.CenterVertically, ) { TokenIcon( - token = token, - screenBackgroundColor = screenBackgroundColor, - iconPlaceholder = if (token.isNative) getActiveIconRes(network.blockchainId) else null, + state = token.tokenIcon, + shouldDisplayNetwork = true, ) - Column(modifier = Modifier.align(Alignment.CenterVertically)) { + Column( + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = TangemTheme.dimens.spacing12), + ) { Text( text = token.name, style = TangemTheme.typography.subtitle1, @@ -213,65 +210,16 @@ private fun TokenItem(token: TokenToSelect, network: Network, screenBackgroundCo } } -@Suppress("MagicNumber") -@Composable -private fun TokenIcon(token: TokenToSelect, screenBackgroundColor: Color, @DrawableRes iconPlaceholder: Int?) { - var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } - val isDarkTheme = isSystemInDarkTheme() - val coroutineScope = rememberCoroutineScope() - - val data = token.iconUrl.ifEmpty { - iconPlaceholder - } - Box( - modifier = Modifier - .padding(end = TangemTheme.dimens.spacing12) - .background( - color = iconBackgroundColor, - shape = TangemTheme.shapes.roundedCorners8, - ), - ) { - val iconModifier = Modifier - .size(TangemTheme.dimens.size40) - val colorFilter = if (!token.available) { - val matrix = ColorMatrix().apply { setToSaturation(0f) } - ColorFilter.colorMatrix(matrix) - } else { - null - } - SubcomposeAsyncImage( - modifier = iconModifier, - model = ImageRequest.Builder(LocalContext.current) - .data(data) - .crossfade(true) - .allowHardware(false) - .listener( - onSuccess = { _, result -> - if (isDarkTheme) { - coroutineScope.launch { - val color = ImageBackgroundContrastChecker( - drawable = result.drawable, - backgroundColor = screenBackgroundColor.toArgb(), - ).getContrastColorIfNeeded(isDarkTheme) - iconBackgroundColor = color - } - } - }, - ).build(), - contentDescription = token.id, - loading = { CircleShimmer(modifier = iconModifier) }, - error = { CurrencyPlaceholderIcon(modifier = iconModifier, id = token.id) }, - alpha = if (!token.available) 0.7f else 1f, - colorFilter = colorFilter, - ) - } -} - -private val token = TokenToSelect( +private val token = TokenToSelectState.TokenToSelect( + tokenIcon = TokenIconState.CoinIcon( + url = "", + fallbackResId = 0, + isGrayscale = false, + showCustomBadge = false, + ), id = "", name = "USDC", symbol = "USDC", - iconUrl = "", isNative = false, addedTokenBalanceData = TokenBalanceData( amount = "15 000 $", @@ -281,17 +229,23 @@ private val token = TokenToSelect( ), ) +private val title = TokenToSelectState.Title( + title = "MY TOKENS", +) + @Preview @Composable private fun TokenScreenPreview() { - SwapSelectTokenScreen( - state = SwapSelectTokenStateHolder( - addedTokens = listOf(token, token, token), - otherTokens = listOf(token, token, token), - onSearchEntered = {}, - onTokenSelected = {}, - network = Network("Ethereum", "ETH"), - ), - onBack = {}, - ) + TangemTheme(isDark = false) { + SwapSelectTokenScreen( + state = SwapSelectTokenStateHolder( + availableTokens = listOf(title, token, token, token).toImmutableList(), + unavailableTokens = listOf(title, token, token, token).toImmutableList(), + onSearchEntered = {}, + onTokenSelected = {}, + network = Network("Ethereum", "ETH"), + ), + onBack = {}, + ) + } } \ No newline at end of file From e9ab4751aff919588f05d9e5c81874143af45c1e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Nov 2023 19:34:10 +0300 Subject: [PATCH 014/139] Updated on 2026-08-14 --- .../swap/models/states/ProviderState.kt | 16 ++ .../tangem/feature/swap/ui/ProviderItem.kt | 245 ++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt new file mode 100644 index 0000000000..6b878743e1 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.swap.models.states + +sealed class ProviderState { + + object Loading : ProviderState() + + data class Content( + val id: String, + val name: String, + val type: String, + val iconUrl: String, + val isBestTrade: Boolean, + val rate: String, + val onProviderClick: () -> Unit, + ) : ProviderState() +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt new file mode 100644 index 0000000000..5dee0e3ff7 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -0,0 +1,245 @@ +package com.tangem.feature.swap.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +import androidx.compose.material.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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.R +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.models.states.ProviderState + +/** + * UI Item for swap provider + * + * https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7856-41909&mode=design&t=vo7dyElitnzSPSW3-4 + */ +@Composable +fun ProviderItem(state: ProviderState) { + when (state) { + is ProviderState.Content -> { + ContentProviderState(state = state) + } + is ProviderState.Loading -> { + LoadingProviderState() + } + } +} + +@Composable +private fun BaseContainer(onClick: (() -> Unit)? = null, content: @Composable BoxScope.() -> Unit) { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .clickable( + enabled = onClick != null, + onClick = { onClick?.invoke() }, + ) + .fillMaxWidth() + .defaultMinSize(minHeight = TangemTheme.dimens.size68), + ) { + content() + } +} + +@Composable +private fun ContentProviderState(state: ProviderState.Content) { + BaseContainer(state.onProviderClick) { + Row( + modifier = Modifier.align(Alignment.CenterStart), + ) { + SubcomposeAsyncImage( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .size(size = TangemTheme.dimens.size40) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current) + .data(state.iconUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + error = { + ErrorProviderIcon( + Modifier.size( + size = TangemTheme.dimens.size40, + ), + ) + }, + contentDescription = null, + ) + + Column( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), + ) { + Row { + Text( + text = state.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.type, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + if (state.isBestTrade) { + BestTradeItem(Modifier.padding(start = TangemTheme.dimens.spacing4)) + } + } + Text( + text = state.rate, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) + } + } + + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + modifier = Modifier + .align(alignment = Alignment.CenterEnd) + .padding(end = TangemTheme.dimens.spacing12), + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Composable +private fun LoadingProviderState() { + BaseContainer { + Column( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(vertical = TangemTheme.dimens.spacing12), + ) { + Text( + text = "Provider", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), + ) + + Row( + modifier = Modifier.padding( + top = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + ), + ) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size16), + color = TangemTheme.colors.icon.informative, + ) + Text( + text = "Fetching best rates ...", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + } + } + + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + modifier = Modifier + .align(alignment = Alignment.CenterEnd) + .padding(end = TangemTheme.dimens.spacing12), + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Composable +private fun ErrorProviderIcon(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCorners8, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.ic_custom_token_44), + contentDescription = null, + ) + } +} + +@Composable +private fun BestTradeItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), + shape = TangemTheme.shapes.roundedCornersLarge, + ), + ) { + Text( + text = "Best trade", + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.icon.accent, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), + ) + } +} + +@Preview +@Composable +private fun ProviderItem_Loading_Preview() { + Column { + TangemTheme(isDark = false) { + ProviderItem(state = ProviderState.Loading) + } + + SpacerH24() + + TangemTheme(isDark = true) { + ProviderItem(state = ProviderState.Loading) + } + } +} + +@Preview +@Composable +private fun ProviderItem_Content_Preview() { + val state = ProviderState.Content( + id = "1", + name = "1inch", + type = "DEX", + iconUrl = "", + isBestTrade = true, + rate = "1 000 000", + onProviderClick = {}, + ) + Column { + TangemTheme(isDark = false) { + ProviderItem(state = state) + } + + SpacerH24() + + TangemTheme(isDark = true) { + ProviderItem(state = state) + } + } +} \ No newline at end of file From 9aa49f2357eff023029a00c316aedd2182b07de8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 15 Nov 2023 15:15:19 +0300 Subject: [PATCH 015/139] Updated on 2026-08-14 --- .../core/ui/components/rows/ActionRow.kt | 78 +++++++++++++++++++ .../components/rows/states/ActionRowState.kt | 7 ++ .../states/ChooseFeeBottomSheetConfig.kt | 5 ++ .../swap/models/states/FeeItemState.kt | 8 ++ .../feature/swap/ui/ChooseFeeBottomSheet.kt | 26 +++++++ .../com/tangem/feature/swap/ui/FeeItem.kt | 61 +++++++++++++++ 6 files changed, 185 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/states/ActionRowState.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt new file mode 100644 index 0000000000..4dd409c78f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt @@ -0,0 +1,78 @@ +package com.tangem.core.ui.components.rows + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH28 +import com.tangem.core.ui.components.rows.states.ActionRowState +import com.tangem.core.ui.res.TangemTheme + +/** + * Simple clickable action row, without input and icon + * + * https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=Ygv5sohTTHYAQcBS-4 + */ +@Composable +fun SimpleActionRow(state: ActionRowState, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background(color = TangemTheme.colors.background.action) + .height(TangemTheme.dimens.size44) + .fillMaxWidth(), + ) { + Column( + modifier = Modifier + .padding(end = TangemTheme.dimens.spacing48) + .align(Alignment.CenterStart), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Text( + text = state.title, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = state.description, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } + + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + modifier = Modifier + .align(alignment = Alignment.CenterEnd) + .padding(end = TangemTheme.dimens.spacing12), + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Preview +@Composable +private fun SimpleActionRowPreview() { + val state = ActionRowState( + title = "Title", + description = "Description", + onClick = {}, + ) + Column { + TangemTheme(isDark = false) { + SimpleActionRow(state) + } + + SpacerH28() + + TangemTheme(isDark = false) { + SimpleActionRow(state) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/states/ActionRowState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/states/ActionRowState.kt new file mode 100644 index 0000000000..35c6097d3a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/states/ActionRowState.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.components.rows.states + +data class ActionRowState( + val title: String, + val description: String, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt new file mode 100644 index 0000000000..f12f22bd79 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt @@ -0,0 +1,5 @@ +package com.tangem.feature.swap.models.states + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +class ChooseFeeBottomSheetConfig : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt new file mode 100644 index 0000000000..be4e3a3366 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.swap.models.states + +import com.tangem.core.ui.components.rows.states.ActionRowState + +data class FeeItemState( + val id: String, + val actionRowState: ActionRowState, +) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt new file mode 100644 index 0000000000..15d2f05826 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.swap.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig + +@Composable +fun ChooseFeeBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet(config) { content: ChooseFeeBottomSheetConfig -> + ChooseFeeBottomSheetContent(content = content) + } +} + +@Suppress("UnusedPrivateMember") +@Composable +private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) { + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt new file mode 100644 index 0000000000..4f895f7d34 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt @@ -0,0 +1,61 @@ +package com.tangem.feature.swap.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.rows.SimpleActionRow +import com.tangem.core.ui.components.rows.states.ActionRowState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.models.states.FeeItemState + +@Composable +fun FeeItem(state: FeeItemState) { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .clickable( + onClick = state.actionRowState.onClick, + ) + .fillMaxWidth() + .defaultMinSize(minHeight = TangemTheme.dimens.size68), + ) { + SimpleActionRow( + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + ), + state = state.actionRowState, + ) + } +} + +@Preview +@Composable +private fun FeeItemPreview() { + val state = FeeItemState( + id = "id", + actionRowState = ActionRowState( + title = "Title", + description = "Description", + onClick = {}, + ), + ) + Column { + TangemTheme(isDark = false) { + FeeItem(state = state) + } + + SpacerH24() + + TangemTheme(isDark = true) { + FeeItem(state = state) + } + } +} \ No newline at end of file From b6cd2781ab1ec858165663d00dbb118cab694a44 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 15 Nov 2023 18:22:53 +0300 Subject: [PATCH 016/139] Updated on 2026-08-14 --- .../states/ChooseProviderBottomSheetConfig.kt | 9 + .../swap/models/states/ProviderState.kt | 35 ++- .../swap/ui/ChooseProviderBottomSheet.kt | 103 +++++++ .../tangem/feature/swap/ui/ProviderItem.kt | 270 ++++++++++++++---- 4 files changed, 361 insertions(+), 56 deletions(-) create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt new file mode 100644 index 0000000000..7507686111 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.feature.swap.models.states + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList + +class ChooseProviderBottomSheetConfig( + val selectedProviderId: String, + val providers: ImmutableList, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index 6b878743e1..f78f6d6cb3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -2,15 +2,42 @@ package com.tangem.feature.swap.models.states sealed class ProviderState { - object Loading : ProviderState() + abstract val onProviderClick: ((String) -> Unit)? + abstract val id: String + + data class Loading( + override val id: String = "", + override val onProviderClick: ((String) -> Unit)? = null, + ) : ProviderState() data class Content( - val id: String, + override val id: String, val name: String, val type: String, val iconUrl: String, - val isBestTrade: Boolean, val rate: String, - val onProviderClick: () -> Unit, + val selectionType: SelectionType, + val additionalBadge: AdditionalBadge, + val percentLowerThenBest: Float?, + override val onProviderClick: (String) -> Unit, ) : ProviderState() + + data class Unavailable( + override val id: String, + val name: String, + val type: String, + val iconUrl: String, + val alertText: String, + override val onProviderClick: ((String) -> Unit)? = null, + ) : ProviderState() + + sealed class AdditionalBadge { + object BestTrade : AdditionalBadge() + object Empty : AdditionalBadge() + object PermissionRequired : AdditionalBadge() + } + + enum class SelectionType { + NONE, CLICK, SELECT + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt new file mode 100644 index 0000000000..266c1f9b49 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -0,0 +1,103 @@ +package com.tangem.feature.swap.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig +import com.tangem.feature.swap.models.states.ProviderState +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet(config) { content: ChooseProviderBottomSheetConfig -> + ChooseProviderBottomSheetContent(content = content) + } +} + +@Composable +private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetConfig) { + Column( + modifier = Modifier.background(TangemTheme.colors.background.secondary), + ) { + Text( + text = "Choose provider", + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing10) + .align(Alignment.CenterHorizontally), + ) + Text( + text = "Providers facilitate transactions, ensuring smooth and efficient token exchanges", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing10) + .padding(horizontal = TangemTheme.dimens.spacing56) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center, + ) + Column( + modifier = Modifier + .padding(TangemTheme.dimens.spacing16) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) { + content.providers.forEach { + val isSelected = it.id == content.selectedProviderId + ProviderItem( + state = it, + isSelected = isSelected, + modifier = Modifier.padding( + horizontal = TangemTheme.dimens.spacing12, + vertical = TangemTheme.dimens.spacing12, + ), + ) + } + } + } +} + +@Preview +@Composable +private fun ChooseProviderBottomSheet_Preview() { + val providers = listOf( + ProviderState.Content( + id = "1", + name = "1inch", + type = "DEX", + iconUrl = "", + rate = "1 000 000", + additionalBadge = ProviderState.AdditionalBadge.BestTrade, + percentLowerThenBest = -1.0f, + selectionType = ProviderState.SelectionType.SELECT, + onProviderClick = {}, + ), + ProviderState.Unavailable( + id = "2", + name = "1inch", + type = "DEX", + iconUrl = "", + alertText = "Unavailable", + ), + ) + TangemTheme(isDark = false) { + ChooseProviderBottomSheetContent( + ChooseProviderBottomSheetConfig( + selectedProviderId = "1", + providers = providers.toImmutableList(), + ), + ) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 5dee0e3ff7..6602b25ca8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -10,6 +10,8 @@ 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.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview @@ -22,47 +24,58 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.states.ProviderState /** - * UI Item for swap provider + * UI Item for swap provider wrapped in a [BaseContainer] with rounded corners * * https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7856-41909&mode=design&t=vo7dyElitnzSPSW3-4 */ + +private const val GRAY_SCALE_SATURATION = 0f +private const val GRAY_SCALE_ALPHA = 0.4f +private val GrayscaleColorFilter: ColorFilter + get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) + @Composable -fun ProviderItem(state: ProviderState) { +fun ProviderItemBlock(state: ProviderState) { + BaseContainer(state) { + ProviderItem( + state = state, + modifier = Modifier.align(Alignment.CenterStart), + ) + } +} + +@Composable +fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected: Boolean = false) { when (state) { is ProviderState.Content -> { - ContentProviderState(state = state) + ProviderContentState( + state = state, + modifier = modifier, + isSelected = isSelected, + ) } is ProviderState.Loading -> { - LoadingProviderState() + ProviderLoadingState( + modifier = modifier, + ) + } + is ProviderState.Unavailable -> { + ProviderUnavailableState( + state = state, + modifier = modifier, + ) } } } @Composable -private fun BaseContainer(onClick: (() -> Unit)? = null, content: @Composable BoxScope.() -> Unit) { - Box( - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .clickable( - enabled = onClick != null, - onClick = { onClick?.invoke() }, - ) - .fillMaxWidth() - .defaultMinSize(minHeight = TangemTheme.dimens.size68), - ) { - content() - } -} - -@Composable -private fun ContentProviderState(state: ProviderState.Content) { - BaseContainer(state.onProviderClick) { - Row( - modifier = Modifier.align(Alignment.CenterStart), - ) { +private fun ProviderContentState( + state: ProviderState.Content, + modifier: Modifier = Modifier, + isSelected: Boolean = false, +) { + Box(modifier = modifier.fillMaxWidth()) { + Row { SubcomposeAsyncImage( modifier = Modifier .padding(start = TangemTheme.dimens.spacing12) @@ -99,38 +112,99 @@ private fun ContentProviderState(state: ProviderState.Content) { color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), ) - if (state.isBestTrade) { - BestTradeItem(Modifier.padding(start = TangemTheme.dimens.spacing4)) + when (state.additionalBadge) { + ProviderState.AdditionalBadge.BestTrade -> + BestTradeItem(Modifier.padding(start = TangemTheme.dimens.spacing4)) + ProviderState.AdditionalBadge.PermissionRequired -> + PermissionBadgeItem(Modifier.padding(start = TangemTheme.dimens.spacing4)) + ProviderState.AdditionalBadge.Empty -> { + // no-op + } } } + Row( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) { + Text( + text = state.rate, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + if (state.percentLowerThenBest != null) { + Text( + text = "${state.percentLowerThenBest}%", // todo add to strings + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.warning, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + } + } + } + } + + ProviderChevron(state = state, isSelected = isSelected) + } +} + +@Composable +private fun ProviderUnavailableState(state: ProviderState.Unavailable, modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxWidth()) { + Row { + val (alpha, colorFilter) = GRAY_SCALE_ALPHA to GrayscaleColorFilter + SubcomposeAsyncImage( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .size(size = TangemTheme.dimens.size40) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current) + .data(state.iconUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + error = { + ErrorProviderIcon( + Modifier.size( + size = TangemTheme.dimens.size40, + ), + ) + }, + alpha = alpha, + colorFilter = colorFilter, + contentDescription = null, + ) + + Column( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), + ) { + Row { + Text( + text = state.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + text = state.type, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + } Text( - text = state.rate, + text = state.alertText, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) } } - - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_24), - contentDescription = null, - modifier = Modifier - .align(alignment = Alignment.CenterEnd) - .padding(end = TangemTheme.dimens.spacing12), - tint = TangemTheme.colors.icon.informative, - ) } } @Composable -private fun LoadingProviderState() { - BaseContainer { - Column( - modifier = Modifier - .align(Alignment.CenterStart) - .padding(vertical = TangemTheme.dimens.spacing12), - ) { +private fun ProviderLoadingState(modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxWidth()) { + Column { Text( text = "Provider", style = TangemTheme.typography.caption2, @@ -168,6 +242,56 @@ private fun LoadingProviderState() { } } +@Composable +private fun BoxScope.ProviderChevron(state: ProviderState.Content, isSelected: Boolean) { + when (state.selectionType) { + ProviderState.SelectionType.NONE -> { + /* no-op */ + } + ProviderState.SelectionType.CLICK -> { + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + modifier = Modifier + .align(alignment = Alignment.CenterEnd) + .padding(end = TangemTheme.dimens.spacing12), + tint = TangemTheme.colors.icon.informative, + ) + } + ProviderState.SelectionType.SELECT -> { + if (isSelected) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + contentDescription = null, + modifier = Modifier + .align(alignment = Alignment.CenterEnd) + .padding(end = TangemTheme.dimens.spacing12), + tint = TangemTheme.colors.icon.accent, + ) + } + } + } +} + +@Composable +private fun BaseContainer(state: ProviderState, content: @Composable BoxScope.() -> Unit) { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .clickable( + enabled = state.onProviderClick != null, + onClick = { state.onProviderClick?.invoke(state.id) }, + ) + .fillMaxWidth() + .defaultMinSize(minHeight = TangemTheme.dimens.size68), + ) { + content() + } +} + @Composable private fun ErrorProviderIcon(modifier: Modifier = Modifier) { Box( @@ -203,18 +327,35 @@ private fun BestTradeItem(modifier: Modifier = Modifier) { } } +@Composable +private fun PermissionBadgeItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCornersLarge, + ), + ) { + Text( + text = "Permission required", + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), + ) + } +} + @Preview @Composable private fun ProviderItem_Loading_Preview() { Column { TangemTheme(isDark = false) { - ProviderItem(state = ProviderState.Loading) + ProviderItemBlock(state = ProviderState.Loading()) } SpacerH24() TangemTheme(isDark = true) { - ProviderItem(state = ProviderState.Loading) + ProviderItemBlock(state = ProviderState.Loading()) } } } @@ -227,19 +368,44 @@ private fun ProviderItem_Content_Preview() { name = "1inch", type = "DEX", iconUrl = "", - isBestTrade = true, rate = "1 000 000", + additionalBadge = ProviderState.AdditionalBadge.PermissionRequired, + percentLowerThenBest = -1.0f, + selectionType = ProviderState.SelectionType.SELECT, onProviderClick = {}, ) Column { TangemTheme(isDark = false) { - ProviderItem(state = state) + ProviderItemBlock(state = state) } SpacerH24() TangemTheme(isDark = true) { - ProviderItem(state = state) + ProviderItemBlock(state = state) + } + } +} + +@Preview +@Composable +private fun ProviderItem_Unavailable_Preview() { + val state = ProviderState.Unavailable( + id = "1", + name = "1inch", + type = "DEX", + iconUrl = "", + alertText = "Unavailable", + ) + Column { + TangemTheme(isDark = false) { + ProviderItemBlock(state = state) + } + + SpacerH24() + + TangemTheme(isDark = true) { + ProviderItemBlock(state = state) } } } \ No newline at end of file From 2f6c106bd7bbe13691e916811ecfea99718a6b5b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Nov 2023 11:33:30 +0300 Subject: [PATCH 017/139] Updated on 2026-08-14 --- .../core/ui/components/rows/ActionRow.kt | 22 +-- .../ui/components/rows/SelectorRowItem.kt | 160 ++++++++++++++++++ .../components/rows/states/ActionRowState.kt | 7 - .../tangem/feature/swap/models/UiActions.kt | 4 + .../states/ChooseFeeBottomSheetConfig.kt | 8 +- .../swap/models/states/FeeItemState.kt | 11 +- .../feature/swap/ui/ChooseFeeBottomSheet.kt | 126 +++++++++++++- .../com/tangem/feature/swap/ui/FeeItem.kt | 21 ++- .../feature/swap/viewmodels/SwapViewModel.kt | 3 + 9 files changed, 329 insertions(+), 33 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/states/ActionRowState.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt index 4dd409c78f..5bdaa8f68b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt @@ -11,7 +11,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH28 -import com.tangem.core.ui.components.rows.states.ActionRowState import com.tangem.core.ui.res.TangemTheme /** @@ -20,7 +19,7 @@ import com.tangem.core.ui.res.TangemTheme * https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=Ygv5sohTTHYAQcBS-4 */ @Composable -fun SimpleActionRow(state: ActionRowState, modifier: Modifier = Modifier) { +fun SimpleActionRow(title: String, description: String, modifier: Modifier = Modifier) { Box( modifier = modifier .background(color = TangemTheme.colors.background.action) @@ -34,12 +33,12 @@ fun SimpleActionRow(state: ActionRowState, modifier: Modifier = Modifier) { verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { Text( - text = state.title, + text = title, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, ) Text( - text = state.description, + text = description, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, ) @@ -59,20 +58,21 @@ fun SimpleActionRow(state: ActionRowState, modifier: Modifier = Modifier) { @Preview @Composable private fun SimpleActionRowPreview() { - val state = ActionRowState( - title = "Title", - description = "Description", - onClick = {}, - ) Column { TangemTheme(isDark = false) { - SimpleActionRow(state) + SimpleActionRow( + title = "Title", + description = "Description", + ) } SpacerH28() TangemTheme(isDark = false) { - SimpleActionRow(state) + SimpleActionRow( + title = "Title", + description = "Description", + ) } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt new file mode 100644 index 0000000000..667ec44c08 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -0,0 +1,160 @@ +package com.tangem.core.ui.components.rows + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun SelectorRowItem( + @StringRes titleRes: Int, + @DrawableRes iconRes: Int, + onSelect: () -> Unit, + modifier: Modifier = Modifier, + preEllipsize: TextReference? = null, + postEllipsize: TextReference? = null, + isSelected: Boolean = false, + showDivider: Boolean = true, +) { + val iconTint by animateColorAsState( + targetValue = if (isSelected) { + TangemTheme.colors.icon.accent + } else { + TangemTheme.colors.icon.informative + }, + label = "Selector icon tint change", + ) + + val textStyle = if (isSelected) { + TangemTheme.typography.subtitle2 + } else { + TangemTheme.typography.body2 + } + + Box( + modifier = modifier + .fillMaxWidth() + .clickable { onSelect() }, + ) { + Row(modifier = Modifier.fillMaxWidth()) { + Icon( + painter = painterResource(iconRes), + tint = iconTint, + contentDescription = null, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) + Text( + text = stringResource(titleRes), + style = textStyle, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing8, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) + if (preEllipsize != null && postEllipsize != null) { + SelectorValueContent( + amount = preEllipsize, + symbol = postEllipsize, + textStyle = textStyle, + ) + } + } + if (showDivider) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size1) + .padding(horizontal = TangemTheme.dimens.spacing12) + .background(TangemTheme.colors.stroke.primary) + .align(Alignment.BottomCenter), + ) + } + } +} + +@Composable +private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) { + Text( + text = amount.resolveReference(), + style = textStyle, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + modifier = Modifier + .weight(1f) + .padding( + start = TangemTheme.dimens.spacing4, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) + Text( + text = symbol.resolveReference(), + style = textStyle, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing1, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing14, + ), + ) +} + +@Preview +@Composable +private fun SelectorRowItemPreview_Light() { + TangemTheme { + SelectorRowItem( + titleRes = R.string.common_fee_selector_option_slow, + iconRes = R.drawable.ic_tortoise_24, + preEllipsize = TextReference.Str("1000"), + postEllipsize = TextReference.Str("$"), + isSelected = true, + onSelect = { }, + ) + } +} + +@Preview +@Composable +private fun SelectorRowItemPreview_Dark() { + TangemTheme(isDark = true) { + SelectorRowItem( + titleRes = R.string.common_fee_selector_option_slow, + iconRes = R.drawable.ic_tortoise_24, + preEllipsize = TextReference.Str("1000"), + postEllipsize = TextReference.Str("$"), + isSelected = true, + onSelect = { }, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/states/ActionRowState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/states/ActionRowState.kt deleted file mode 100644 index 35c6097d3a..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/states/ActionRowState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.core.ui.components.rows.states - -data class ActionRowState( - val title: String, - val description: String, - val onClick: () -> Unit, -) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index d250e06df6..0dc67646d0 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.models import com.tangem.core.ui.components.states.Item +import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.domain.models.ui.TxFee data class UiActions( @@ -17,4 +18,7 @@ data class UiActions( val hidePermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, val onSelectItemFee: (Item) -> Unit, + // region new actions + val onClickFee: () -> Unit, + val onSelectFeeType: (FeeType) -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt index f12f22bd79..82a1008a27 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt @@ -1,5 +1,11 @@ package com.tangem.feature.swap.models.states import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.feature.swap.domain.models.ui.FeeType +import kotlinx.collections.immutable.ImmutableList -class ChooseFeeBottomSheetConfig : TangemBottomSheetConfigContent \ No newline at end of file +class ChooseFeeBottomSheetConfig( + val selectedFee: FeeType, + val onSelectFeeType: (FeeType) -> Unit, + val feeItems: ImmutableList, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt index be4e3a3366..beed7112a6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt @@ -1,8 +1,13 @@ package com.tangem.feature.swap.models.states -import com.tangem.core.ui.components.rows.states.ActionRowState +import com.tangem.feature.swap.domain.models.ui.FeeType data class FeeItemState( - val id: String, - val actionRowState: ActionRowState, + val feeType: FeeType, + val title: String, + val amountCrypto: String, + val symbolCrypto: String, + val amountFiat: String, + val symbolFiat: String, + val onClick: () -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 15d2f05826..f967baef74 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -2,12 +2,24 @@ package com.tangem.feature.swap.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig +import com.tangem.feature.swap.models.states.FeeItemState +import com.tangem.feature.swap.presentation.R +import kotlinx.collections.immutable.toImmutableList @Composable fun ChooseFeeBottomSheet(config: TangemBottomSheetConfig) { @@ -16,11 +28,121 @@ fun ChooseFeeBottomSheet(config: TangemBottomSheetConfig) { } } -@Suppress("UnusedPrivateMember") @Composable private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { Column( - modifier = Modifier.background(TangemTheme.colors.background.primary), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) { + Text( + text = "Choose fee", // todo replace with strings + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing10) + .align(Alignment.CenterHorizontally), + ) + Column( + modifier = Modifier + .padding(TangemTheme.dimens.spacing16) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) { + FeeItemsBlock(content) + } + Text( + text = "Network transaction fees are small charges paid to support network security, incentivize " + + "validators," + + " allocate resources, and determine transaction priority.", // todo replace with strings + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .padding( + vertical = TangemTheme.dimens.spacing8, + horizontal = TangemTheme.dimens.spacing16, + ) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Start, + ) + } +} + +@Composable +private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { + content.feeItems.forEach { feeItem -> + val isSelected = feeItem.feeType == content.selectedFee + val preEllipsizeText = feeItem.amountCrypto + val postEllipsizeText = " ${feeItem.symbolCrypto} (${feeItem.amountFiat} ${feeItem.symbolFiat})" + when (feeItem.feeType) { + FeeType.NORMAL -> { + SelectorRowItem( + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, + preEllipsize = TextReference.Str(preEllipsizeText), + postEllipsize = TextReference.Str(postEllipsizeText), + isSelected = isSelected, + onSelect = { content.onSelectFeeType(feeItem.feeType) }, + ) + } + FeeType.PRIORITY -> { + SelectorRowItem( + titleRes = R.string.common_fee_selector_option_fast, + iconRes = R.drawable.ic_hare_24, + preEllipsize = TextReference.Str(preEllipsizeText), + postEllipsize = TextReference.Str(postEllipsizeText), + isSelected = isSelected, + onSelect = { content.onSelectFeeType(feeItem.feeType) }, + ) + } + } + } +} + +@Preview +@Composable +private fun ChooseFeeBottomSheetContent_Preview() { + val feeItems = listOf( + FeeItemState( + feeType = FeeType.NORMAL, + title = "Fee", + amountCrypto = "1000", + symbolCrypto = "MATIC", + amountFiat = "10", + symbolFiat = "$", + onClick = {}, + ), + FeeItemState( + feeType = FeeType.PRIORITY, + title = "Fee", + amountCrypto = "2000", + symbolCrypto = "MATIC", + amountFiat = "20", + symbolFiat = "$", + onClick = {}, + ), + ).toImmutableList() + Column { + TangemTheme(isDark = true) { + ChooseFeeBottomSheetContent( + ChooseFeeBottomSheetConfig( + selectedFee = FeeType.NORMAL, + onSelectFeeType = {}, + feeItems = feeItems, + ), + ) + } + + SpacerH24() + + TangemTheme(isDark = false) { + ChooseFeeBottomSheetContent( + ChooseFeeBottomSheetConfig( + selectedFee = FeeType.NORMAL, + onSelectFeeType = {}, + feeItems = feeItems, + ), + ) + } } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt index 4f895f7d34..c1d2976921 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt @@ -8,8 +8,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.rows.SimpleActionRow -import com.tangem.core.ui.components.rows.states.ActionRowState import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.FeeItemState @Composable @@ -21,17 +21,19 @@ fun FeeItem(state: FeeItemState) { shape = TangemTheme.shapes.roundedCornersXMedium, ) .clickable( - onClick = state.actionRowState.onClick, + onClick = state.onClick, ) .fillMaxWidth() .defaultMinSize(minHeight = TangemTheme.dimens.size68), ) { + val description = "${state.amountCrypto} ${state.symbolCrypto} (${state.amountFiat} ${state.symbolFiat})" SimpleActionRow( modifier = Modifier.padding( start = TangemTheme.dimens.spacing12, top = TangemTheme.dimens.spacing12, ), - state = state.actionRowState, + title = state.title, + description = description, ) } } @@ -40,12 +42,13 @@ fun FeeItem(state: FeeItemState) { @Composable private fun FeeItemPreview() { val state = FeeItemState( - id = "id", - actionRowState = ActionRowState( - title = "Title", - description = "Description", - onClick = {}, - ), + feeType = FeeType.NORMAL, + title = "Fee", + amountCrypto = "1000", + symbolCrypto = "MATIC", + amountFiat = "10", + symbolFiat = "$", + onClick = {}, ) Column { TangemTheme(isDark = false) { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index c9c2686539..71c151673d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -443,6 +443,7 @@ internal class SwapViewModel @Inject constructor( } } + @Suppress("LongMethod") private fun createUiActions(): UiActions { return UiActions( onSearchEntered = { onSearchEntered(it) }, @@ -510,6 +511,8 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.updateFeeSelectedItem(uiState, feeItem, isFeeEnough) } }, + onClickFee = {}, + onSelectFeeType = {}, ) } From 75075b53c8b5c7026be3643aec946f31be022531 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Nov 2023 14:04:21 +0200 Subject: [PATCH 018/139] Updated on 2026-08-14 --- .../tokens/GetCryptoCurrencyStatusUseCase.kt | 52 +++++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 22 +- .../swap/domain/BlockchainInteractor.kt | 4 +- ...Impl.kt => DefaultBlockchainInteractor.kt} | 12 +- .../feature/swap/domain/SwapInteractor.kt | 26 +-- .../feature/swap/domain/SwapInteractorImpl.kt | 218 +++++++++++------- .../feature/swap/domain/SwapRepository.kt | 6 +- .../swap/domain/cache/SwapDataCache.kt | 10 +- .../swap/domain/cache/SwapDataCacheImpl.kt | 14 +- .../converters/SwapCurrencyConverter.kt | 49 +--- .../swap/domain/di/SwapDomainModule.kt | 46 +++- .../domain/models/domain/PermissionOptions.kt | 3 +- .../models/ui/TokensDataStateExpress.kt | 31 +++ .../swap/converters/TokensDataConverter.kt | 16 +- .../feature/swap/models/SwapStateHolder.kt | 2 +- .../tangem/feature/swap/ui/StateBuilder.kt | 25 +- .../feature/swap/ui/SwapScreenContent.kt | 4 +- .../feature/swap/viewmodels/SwapViewModel.kt | 101 ++++---- 18 files changed, 412 insertions(+), 229 deletions(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt rename features/swap/domain/src/main/java/com/tangem/feature/swap/domain/{BlockchainInteractorImpl.kt => DefaultBlockchainInteractor.kt} (76%) create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt new file mode 100644 index 0000000000..51029ebe6e --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt @@ -0,0 +1,52 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.error.mapper.mapToTokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.TokenListOperations +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* + +class GetCryptoCurrencyStatusUseCase( + internal val currenciesRepository: CurrenciesRepository, + internal val quotesRepository: QuotesRepository, + internal val networksRepository: NetworksRepository, + internal val dispatchers: CoroutineDispatcherProvider, +) { + + operator fun invoke(userWalletId: UserWalletId): Flow>> { + val operations = CurrenciesStatusesOperations( + userWalletId = userWalletId, + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + ) + + return operations.getCurrenciesStatusesFlow() + .map { maybeCurrenciesStatuses -> + maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) + } + } + + private fun createTokenList( + userWalletId: UserWalletId, + tokens: List, + ): Flow> { + val operations = TokenListOperations( + userWalletId = userWalletId, + tokens = tokens, + currenciesRepository = currenciesRepository, + ) + + return operations.getTokenListFlow().map { maybeTokenList -> + maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError) + } + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 53055fe8d4..514ea79360 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -193,26 +193,26 @@ internal class SwapRepositoryImpl @Inject constructor( override suspend fun getCryptoCurrency( userWallet: UserWallet, - currency: Currency, + currency: CryptoCurrency, network: Network, ): CryptoCurrency? { - val blockchain = Blockchain.fromNetworkId(currency.networkId) ?: return null + val blockchain = Blockchain.fromNetworkId(currency.network.id.value) ?: return null val cryptoCurrencyFactory = CryptoCurrencyFactory() return when (currency) { - is Currency.NativeToken -> { + is CryptoCurrency.Coin -> { cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = network.derivationPath.value, derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, ) } - is Currency.NonNativeToken -> { + is CryptoCurrency.Token -> { val sdkToken = SdkToken( name = currency.name, symbol = currency.symbol, contractAddress = currency.contractAddress, - decimals = currency.decimalCount, - id = currency.id, + decimals = currency.decimals, + id = currency.id.value, ) cryptoCurrencyFactory.createToken( sdkToken = sdkToken, @@ -258,7 +258,7 @@ internal class SwapRepositoryImpl @Inject constructor( userWalletId: UserWalletId, networkId: String, derivationPath: String?, - currency: Currency, + currency: CryptoCurrency, amount: BigDecimal?, ): String { val blockchain = @@ -276,16 +276,16 @@ internal class SwapRepositoryImpl @Inject constructor( ) ?: error("Cannot cast to Approver") } - private fun convertToAmount(amount: BigDecimal, currency: Currency, blockchain: Blockchain): Amount { + private fun convertToAmount(amount: BigDecimal, currency: CryptoCurrency, blockchain: Blockchain): Amount { return when (currency) { - is Currency.NativeToken -> { + is CryptoCurrency.Token -> { Amount(value = amount, blockchain = blockchain) } - is Currency.NonNativeToken -> { + is CryptoCurrency.Coin -> { Amount( currencySymbol = currency.symbol, value = amount, - decimals = currency.decimalCount, + decimals = currency.decimals, ) } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt index 9830aea157..229911c654 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt @@ -1,11 +1,11 @@ package com.tangem.feature.swap.domain -import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.NetworkInfo interface BlockchainInteractor { - fun getTokenDecimals(token: Currency): Int + fun getTokenDecimals(token: CryptoCurrency): Int /** * In app blockchain id, actual in blockchain sdk, not the same as networkId diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt similarity index 76% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractorImpl.kt rename to features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt index 969b20c5b3..0b9605485b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt @@ -1,11 +1,11 @@ package com.tangem.feature.swap.domain -import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.NetworkInfo import com.tangem.lib.crypto.TransactionManager import javax.inject.Inject -internal class BlockchainInteractorImpl @Inject constructor( +internal class DefaultBlockchainInteractor @Inject constructor( private val transactionManager: TransactionManager, ) : BlockchainInteractor { @@ -23,11 +23,11 @@ internal class BlockchainInteractorImpl @Inject constructor( return transactionManager.getExplorerTransactionLink(networkId, txAddress) } - override fun getTokenDecimals(token: Currency): Int { - return if (token is Currency.NonNativeToken) { - token.decimalCount + override fun getTokenDecimals(token: CryptoCurrency): Int { + return if (token is CryptoCurrency.Token) { + token.decimals } else { - transactionManager.getNativeTokenDecimals(token.networkId) + transactionManager.getNativeTokenDecimals(token.network.id.value) } } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 53c62fc7ea..13d3c2d3f4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -9,9 +9,7 @@ import java.math.BigDecimal interface SwapInteractor { - suspend fun getPairs(currency: Currency): List - - suspend fun getPairs(initialCurrency: LeastTokenInfo, currenciesList: List): List + suspend fun getTokensDataState(currency: Currency): TokensDataStateExpress fun initDerivationPathAndNetwork(derivationPath: String?, network: Network?) @@ -31,17 +29,17 @@ interface SwapInteractor { * * @param networkId networkId for tokens * @param searchQuery string query for search - * @return [FoundTokensState] that contains list of tokens matching condition query + * @return [FoundTokensStateExpress] that contains list of tokens matching condition query */ - suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensState + suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensStateExpress /** * Find specific token by id, null if not found * * @param id token id - * @return [Currency] or null + * @return [CryptoCurrency] or null */ - fun findTokenById(id: String): Currency? + fun findTokenById(id: String): CryptoCurrency? /** * Gives permission to swap, this starts scan card process @@ -66,8 +64,8 @@ interface SwapInteractor { @Throws(IllegalStateException::class) suspend fun findBestQuote( networkId: String, - fromToken: Currency, - toToken: Currency, + fromToken: CryptoCurrency, + toToken: CryptoCurrency, amountToSwap: String, selectedFee: FeeType = FeeType.NORMAL, ): SwapState @@ -88,8 +86,8 @@ interface SwapInteractor { suspend fun onSwap( networkId: String, swapStateData: SwapStateData, - currencyToSend: Currency, - currencyToGet: Currency, + currencyToSend: CryptoCurrency, + currencyToGet: CryptoCurrency, amountToSwap: String, fee: TxFee, ): TxState @@ -100,16 +98,16 @@ interface SwapInteractor { * @param networkId * @param token */ - fun getTokenBalance(networkId: String, token: Currency): SwapAmount + fun getTokenBalance(networkId: String, token: CryptoCurrency): SwapAmount fun isAvailableToSwap(networkId: String): Boolean - fun getSwapAmountForToken(amount: String, token: Currency): SwapAmount + fun getSwapAmountForToken(amount: String, token: CryptoCurrency): SwapAmount suspend fun checkFeeIsEnough( fee: BigDecimal?, spendAmount: SwapAmount, networkId: String, - fromToken: Currency, + fromToken: CryptoCurrency, ): Boolean } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 9f1ef22479..1381d354c9 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,12 +1,13 @@ package com.tangem.feature.swap.domain import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import arrow.core.getOrElse +import com.tangem.domain.tokens.GetCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.cache.SwapDataCache import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter @@ -21,6 +22,7 @@ import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.toFiatString +import kotlinx.coroutines.flow.first import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -37,7 +39,7 @@ internal class SwapInteractorImpl @Inject constructor( private val networksRepository: NetworksRepository, private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, + private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusUseCase, ) : SwapInteractor { // TODO: Move to DI @@ -50,26 +52,65 @@ internal class SwapInteractorImpl @Inject constructor( private var derivationPath: String? = null private var network: Network? = null - override suspend fun getPairs(currency: Currency): List { - val currencies = getSelectedWalletSyncUseCase().fold( - ifLeft = { emptyList() }, - ifRight = { selectedWallet -> - getCryptoCurrenciesUseCase(selectedWallet.walletId).fold( - ifLeft = { emptyList() }, - ifRight = { it }, - ) - }, + override suspend fun getTokensDataState(currency: Currency): TokensDataStateExpress { + val selectedWallet = getSelectedWalletSyncUseCase().fold( + ifLeft = { null }, + ifRight = { it }, ) - val pairs = getPairs( + requireNotNull(selectedWallet) + + val currencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) + .first() + .getOrElse { emptyList() } + // .filter { it.currency.network.backendId != currency.networkId } + val currencies = currencyStatuses.map { it.currency } + + val pairsLeast = getPairs( initialCurrency = LeastTokenInfo( contractAddress = (currency as? Currency.NonNativeToken)?.contractAddress ?: "0", network = currency.networkId, ), - currenciesList = currencies, + currenciesList = currencyStatuses.map { it.currency }, ) - return createCryptoCurrencyPairs(pairs, currencies) + val pairs = createCryptoCurrencyPairs(pairsLeast, currencies) + + val initialCryptoCurrency = mapLegacyCurrencyToCryptoCurrency(currency, currencyStatuses) + ?: error("Initial crypto currency must not be null") + + return TokensDataStateExpress( + initialCryptoCurrency = initialCryptoCurrency, + preselectTokens = getPreselectTokens(currency, currencyStatuses), + foundTokensState = FoundTokensStateExpress(emptyList(), emptyList()), + pairs = pairs, + ) + } + + private fun getPreselectTokens(currency: Currency, currencies: List): PreselectTokensExpress { + val from = mapLegacyCurrencyToCryptoCurrency(currency, currencies) + + val to = currencies.firstOrNull()?.currency // TODO choose of 3 variants + + if (from != null && to != null) { + return PreselectTokensExpress( + fromToken = from, + toToken = to, + ) + } else { + error("From and to currencies must not be null") + } + } + + private fun mapLegacyCurrencyToCryptoCurrency( + currency: Currency, + currencies: List, + ): CryptoCurrency? { + return currencies.map { it.currency } + .find { + it.network.backendId == currency.networkId && + it.getContractAddress() == currency.getContractAddress() + } } private fun createCryptoCurrencyPairs( @@ -97,22 +138,35 @@ internal class SwapInteractorImpl @Inject constructor( ): CryptoCurrency? { return cryptoCurrenciesList.find { it.network.backendId == leastTokenInfo.network && - (it as? CryptoCurrency.Token)?.contractAddress ?: "0" == leastTokenInfo.contractAddress + it.getContractAddress() == leastTokenInfo.contractAddress } } - override suspend fun getPairs( - initialCurrency: LeastTokenInfo, - currenciesList: List, - ): List { + private fun CryptoCurrency.getContractAddress(): String { + return when (this) { + is CryptoCurrency.Token -> this.contractAddress + is CryptoCurrency.Coin -> "0" + } + } + + private fun Currency.getContractAddress(): String { + return when (this) { + is Currency.NativeToken -> "0" + is Currency.NonNativeToken -> this.contractAddress + } + } + + suspend fun getPairs(initialCurrency: LeastTokenInfo, currenciesList: List): List { return repository.getPairs(initialCurrency, currenciesList) } + @Deprecated("used in old swap mechanism") override fun initDerivationPathAndNetwork(derivationPath: String?, network: Network?) { this.derivationPath = derivationPath this.network = network } + @Deprecated("used in old swap mechanism") override suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState { // TODO: refactor this function val networkId = initialCurrency.networkId @@ -134,7 +188,7 @@ internal class SwapInteractorImpl @Inject constructor( allLoadedTokens.firstOrNull { it.symbol == token.symbol }?.let { loadedOnWalletsMap.add(it.symbol) it - } ?: swapCurrencyConverter.convertBack(token) + } ?: TODO() } .filter { it.symbol != initialCurrency.symbol && allLoadedTokens.contains(it) } val loadedTokens = allLoadedTokens @@ -146,21 +200,22 @@ internal class SwapInteractorImpl @Inject constructor( val appCurrency = userWalletManager.getUserAppCurrency() val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id }) cache.cacheBalances(networkId, derivationPath, tokensBalance) - cache.cacheLoadedTokens(loadedTokens.map { TokenWithBalance(it) }) - cache.cacheInWalletTokens(getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency)) + // cache.cacheLoadedTokens(loadedTokens.map { TokenWithBalance(it) }) + // cache.cacheInWalletTokens(getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency)) return TokensDataState( preselectTokens = PreselectTokens( fromToken = initialCurrency, toToken = selectToToken(initialCurrency, tokensInWallet, loadedTokens), ), foundTokensState = FoundTokensState( - tokensInWallet = cache.getInWalletTokens(), - loadedTokens = cache.getLoadedTokens(), + tokensInWallet = emptyList(), // cache.getInWalletTokens(), + loadedTokens = emptyList(), // cache.getLoadedTokens(), ), ) } - override suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensState { + @Deprecated("used in old swap mechanism") + override suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensStateExpress { val searchQueryLowerCase = searchQuery.lowercase() val tokensInWallet = cache.getInWalletTokens() .filter { @@ -172,19 +227,21 @@ internal class SwapInteractorImpl @Inject constructor( it.token.name.lowercase().contains(searchQueryLowerCase) || it.token.symbol.lowercase().contains(searchQueryLowerCase) } - return FoundTokensState( + return FoundTokensStateExpress( tokensInWallet = tokensInWallet, loadedTokens = loadedTokens, ) } - override fun findTokenById(id: String): Currency? { + @Deprecated("used in old swap mechanism") + override fun findTokenById(id: String): CryptoCurrency? { val tokensInWallet = cache.getInWalletTokens() val loadedTokens = cache.getLoadedTokens() - return tokensInWallet.firstOrNull { it.token.id == id }?.token - ?: loadedTokens.firstOrNull { it.token.id == id }?.token + return tokensInWallet.firstOrNull { it.token.id.value == id }?.token + ?: loadedTokens.firstOrNull { it.token.id.value == id }?.token } + @Deprecated("used in old swap mechanism") override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState { val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) { getApproveData( @@ -223,10 +280,11 @@ internal class SwapInteractorImpl @Inject constructor( } } + @Deprecated("used in old swap mechanism") override suspend fun findBestQuote( networkId: String, - fromToken: Currency, - toToken: Currency, + fromToken: CryptoCurrency, + toToken: CryptoCurrency, amountToSwap: String, selectedFee: FeeType, ): SwapState { @@ -268,11 +326,12 @@ internal class SwapInteractorImpl @Inject constructor( } } + @Deprecated("used in old swap mechanism") override suspend fun onSwap( networkId: String, swapStateData: SwapStateData, - currencyToSend: Currency, - currencyToGet: Currency, + currencyToSend: CryptoCurrency, + currencyToGet: CryptoCurrency, amountToSwap: String, fee: TxFee, ): TxState { @@ -322,7 +381,8 @@ internal class SwapInteractorImpl @Inject constructor( } } - override fun getTokenBalance(networkId: String, token: Currency): SwapAmount { + @Deprecated("used in old swap mechanism") + override fun getTokenBalance(networkId: String, token: CryptoCurrency): SwapAmount { return cache.getBalanceForToken( networkId = networkId, derivationPath = derivationPath, @@ -330,25 +390,28 @@ internal class SwapInteractorImpl @Inject constructor( ) ?: SwapAmount(BigDecimal.ZERO, getTokenDecimals(token)) } + @Deprecated("used in old swap mechanism") override fun isAvailableToSwap(networkId: String): Boolean { return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId) } - override fun getSwapAmountForToken(amount: String, token: Currency): SwapAmount { + @Deprecated("used in old swap mechanism") + override fun getSwapAmountForToken(amount: String, token: CryptoCurrency): SwapAmount { val amountDecimal = requireNotNull(toBigDecimalOrNull(amount)) { "wrong amount format" } return SwapAmount(amountDecimal, getTokenDecimals(token)) } - private suspend fun onSuccessLegacyFlow(currency: Currency) { + @Deprecated("used in old swap mechanism") + private suspend fun onSuccessLegacyFlow(currency: CryptoCurrency) { userWalletManager.addToken(swapCurrencyConverter.convert(currency), derivationPath) userWalletManager.refreshWallet() } - private suspend fun onSuccessNewFlow(currency: Currency) { - val network = network ?: return + @Deprecated("used in old swap mechanism") + private suspend fun onSuccessNewFlow(currency: CryptoCurrency) { getSelectedWalletSyncUseCase().fold( ifRight = { userWallet -> - getAndAddCryptoCurrency(userWallet, currency, network) + addCryptoCurrenciesUseCase(userWallet.walletId, currency) }, ifLeft = { Timber.e("Swap Error on getSelectedWalletUseCase") @@ -356,24 +419,20 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun getAndAddCryptoCurrency(userWallet: UserWallet, currency: Currency, network: Network) { - repository.getCryptoCurrency(userWallet, currency, network)?.let { cryptoCurrency -> - addCryptoCurrenciesUseCase(userWallet.walletId, cryptoCurrency) - } - } - + @Deprecated("used in old swap mechanism") private fun getTangemFee(): Double { return repository.getTangemFee() } - private fun getTokenDecimals(token: Currency): Int { - return if (token is Currency.NonNativeToken) { - token.decimalCount + private fun getTokenDecimals(token: CryptoCurrency): Int { + return if (token is CryptoCurrency.Token) { + token.decimals } else { - transactionManager.getNativeTokenDecimals(token.networkId) + transactionManager.getNativeTokenDecimals(token.network.id.value) } } + @Deprecated("used in old swap mechanism") private fun selectToToken( initialToken: Currency, tokensInWallet: List, @@ -394,6 +453,7 @@ internal class SwapInteractorImpl @Inject constructor( return toToken } + @Deprecated("used in old swap mechanism") private fun getTokensWithBalance( tokens: List, balances: Map, @@ -418,8 +478,8 @@ internal class SwapInteractorImpl @Inject constructor( } } - private suspend fun isAllowedToSpend(networkId: String, fromToken: Currency, amount: SwapAmount): Boolean { - if (fromToken is Currency.NativeToken) return true + private suspend fun isAllowedToSpend(networkId: String, fromToken: CryptoCurrency, amount: SwapAmount): Boolean { + if (fromToken is CryptoCurrency.Coin) return true return getSelectedWalletSyncUseCase().fold( ifRight = { userWallet -> val allowance = repository.getAllowance( @@ -438,7 +498,11 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private fun createEmptyAmountState(networkId: String, fromToken: Currency, toToken: Currency): SwapState { + private fun createEmptyAmountState( + networkId: String, + fromToken: CryptoCurrency, + toToken: CryptoCurrency, + ): SwapState { val appCurrency = userWalletManager.getUserAppCurrency() val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol) val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol) @@ -462,8 +526,8 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenAddress: String, toTokenAddress: String, amount: SwapAmount, - fromToken: Currency, - toToken: Currency, + fromToken: CryptoCurrency, + toToken: CryptoCurrency, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, ): SwapState { @@ -520,8 +584,8 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, fromTokenAddress: String, toTokenAddress: String, - fromToken: Currency, - toToken: Currency, + fromToken: CryptoCurrency, + toToken: CryptoCurrency, amount: SwapAmount, selectedFee: FeeType, ): SwapState { @@ -585,45 +649,45 @@ internal class SwapInteractorImpl @Inject constructor( @Suppress("LongParameterList") private suspend fun updateBalances( networkId: String, - fromToken: Currency, - toToken: Currency, + fromToken: CryptoCurrency, + toToken: CryptoCurrency, fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, swapStateData: SwapStateData?, ): SwapState.QuotesLoadedState { val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) - val rates = repository.getRates(appCurrency.code, listOf(fromToken.id, toToken.id, nativeToken.id)) + val rates = repository.getRates(appCurrency.code, listOf(fromToken.id.value, toToken.id.value, nativeToken.id)) val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol) val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol) return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( tokenAmount = fromTokenAmount, - coinId = fromToken.id, + coinId = fromToken.id.value, tokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: ZERO_BALANCE, tokenFiatBalance = fromTokenAmount.value.toFiatString( - rateValue = rates[fromToken.id]?.toBigDecimal() ?: BigDecimal.ZERO, + rateValue = rates[fromToken.id.value]?.toBigDecimal() ?: BigDecimal.ZERO, fiatCurrencyName = appCurrency.symbol, formatWithSpaces = true, ), ), toTokenInfo = TokenSwapInfo( tokenAmount = toTokenAmount, - coinId = toToken.id, + coinId = toToken.id.value, tokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: ZERO_BALANCE, tokenFiatBalance = toTokenAmount.value.toFiatString( - rateValue = rates[toToken.id]?.toBigDecimal() ?: BigDecimal.ZERO, + rateValue = rates[toToken.id.value]?.toBigDecimal() ?: BigDecimal.ZERO, fiatCurrencyName = appCurrency.symbol, formatWithSpaces = true, ), ), priceImpact = calculatePriceImpact( fromTokenAmount = fromTokenAmount.value, - fromRate = rates[fromToken.id] ?: 0.0, + fromRate = rates[fromToken.id.value] ?: 0.0, toTokenAmount = toTokenAmount.value, - toRate = rates[toToken.id] ?: 0.0, + toRate = rates[toToken.id.value] ?: 0.0, ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), swapDataModel = swapStateData, @@ -634,7 +698,7 @@ internal class SwapInteractorImpl @Inject constructor( @Suppress("LongParameterList") private suspend fun updatePermissionState( networkId: String, - fromToken: Currency, + fromToken: CryptoCurrency, swapAmount: SwapAmount, quotesLoadedState: SwapState.QuotesLoadedState, ): SwapState.QuotesLoadedState { @@ -691,7 +755,7 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun syncWalletBalanceForTokens(networkId: String, tokens: List) { + private suspend fun syncWalletBalanceForTokens(networkId: String, tokens: List) { val tokensToSync = tokens.filter { cache.getBalanceForToken(networkId, derivationPath, it.symbol) == null } if (tokensToSync.isNotEmpty()) { val tokensBalance = @@ -746,12 +810,12 @@ internal class SwapInteractorImpl @Inject constructor( private fun isBalanceEnough( networkId: String, - fromToken: Currency, + fromToken: CryptoCurrency, amount: SwapAmount, fee: BigDecimal?, ): Boolean { val tokenBalance = getTokenBalance(networkId, fromToken).value - return if (fromToken is Currency.NonNativeToken) { + return if (fromToken is CryptoCurrency.Token) { tokenBalance >= amount.value } else { tokenBalance > amount.value.plus(fee ?: BigDecimal.ZERO) @@ -762,12 +826,12 @@ internal class SwapInteractorImpl @Inject constructor( return userWalletManager.getWalletAddress(networkId, derivationPath) } - private fun getTokenAddress(currency: Currency): String { + private fun getTokenAddress(currency: CryptoCurrency): String { return when (currency) { - is Currency.NativeToken -> { + is CryptoCurrency.Coin -> { DEFAULT_BLOCKCHAIN_INCH_ADDRESS } - is Currency.NonNativeToken -> { + is CryptoCurrency.Token -> { currency.contractAddress } } @@ -777,7 +841,7 @@ internal class SwapInteractorImpl @Inject constructor( fee: BigDecimal?, spendAmount: SwapAmount, networkId: String, - fromToken: Currency, + fromToken: CryptoCurrency, ): Boolean { if (fee == null) { return false @@ -785,12 +849,12 @@ internal class SwapInteractorImpl @Inject constructor( val nativeTokenBalance = userWalletManager.getNativeTokenBalance(networkId, derivationPath) val percentsToFeeIncrease = BigDecimal.ONE return when (fromToken) { - is Currency.NativeToken -> { + is CryptoCurrency.Coin -> { nativeTokenBalance?.let { balance -> return balance.value.minus(spendAmount.value) > fee.multiply(percentsToFeeIncrease) } ?: false } - is Currency.NonNativeToken -> { + is CryptoCurrency.Token -> { nativeTokenBalance?.let { balance -> return balance.value > fee.multiply(percentsToFeeIncrease) } ?: false @@ -816,7 +880,7 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun getApproveData( networkId: String, derivationPath: String?, - fromToken: Currency, + fromToken: CryptoCurrency, swapAmount: SwapAmount? = null, ): String { return getSelectedWalletSyncUseCase().fold( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 3b64564a3b..ab0e4095c8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -1,8 +1,6 @@ package com.tangem.feature.swap.domain import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* @@ -46,8 +44,6 @@ interface SwapRepository { */ fun getTangemFee(): Double - suspend fun getCryptoCurrency(userWallet: UserWallet, currency: Currency, network: Network): CryptoCurrency? - @Throws(IllegalStateException::class) suspend fun getAllowance( userWalletId: UserWalletId, @@ -62,7 +58,7 @@ interface SwapRepository { userWalletId: UserWalletId, networkId: String, derivationPath: String?, - currency: Currency, + currency: CryptoCurrency, amount: BigDecimal?, ): String } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt index 7f4bf919cc..4890b5ca49 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt @@ -2,19 +2,19 @@ package com.tangem.feature.swap.domain.cache import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.feature.swap.domain.models.ui.TokenWithBalance +import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress import java.math.BigDecimal interface SwapDataCache { fun cacheAvailableToSwapTokens(networkId: String, tokens: List) - fun cacheInWalletTokens(tokens: List) - fun cacheLoadedTokens(tokens: List) + fun cacheInWalletTokens(tokens: List) + fun cacheLoadedTokens(tokens: List) fun cacheBalances(networkId: String, derivationPath: String?, balances: Map) fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String) fun getAvailableTokens(networkId: String): List - fun getInWalletTokens(): List - fun getLoadedTokens(): List + fun getInWalletTokens(): List + fun getLoadedTokens(): List fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? fun getLastFeeForNetwork(networkId: String): BigDecimal? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt index 4379d70d9f..2a5d7535ca 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain.cache import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.feature.swap.domain.models.ui.TokenWithBalance +import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress import java.math.BigDecimal class SwapDataCacheImpl : SwapDataCache { @@ -10,28 +10,28 @@ class SwapDataCacheImpl : SwapDataCache { private val availableTokensForNetwork: MutableMap> = mutableMapOf() private val feesForNetworks: MutableMap = mutableMapOf() private val tokensBalances: MutableMap> = mutableMapOf() - private val lastInWalletTokens = mutableListOf() - private val lastLoadedTokens = mutableListOf() + private val lastInWalletTokens = mutableListOf() + private val lastLoadedTokens = mutableListOf() override fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String) { feesForNetworks[networkId] = fee } - override fun cacheInWalletTokens(tokens: List) { + override fun cacheInWalletTokens(tokens: List) { lastInWalletTokens.clear() lastInWalletTokens.addAll(tokens) } - override fun cacheLoadedTokens(tokens: List) { + override fun cacheLoadedTokens(tokens: List) { lastLoadedTokens.clear() lastLoadedTokens.addAll(tokens) } - override fun getInWalletTokens(): List { + override fun getInWalletTokens(): List { return lastInWalletTokens } - override fun getLoadedTokens(): List { + override fun getLoadedTokens(): List { return lastLoadedTokens } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/converters/SwapCurrencyConverter.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/converters/SwapCurrencyConverter.kt index d766f5029a..2cf345a0bb 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/converters/SwapCurrencyConverter.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/converters/SwapCurrencyConverter.kt @@ -1,54 +1,29 @@ package com.tangem.feature.swap.domain.converters -import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.utils.converter.TwoWayConverter -import com.tangem.lib.crypto.models.Currency as CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.lib.crypto.models.Currency +import com.tangem.utils.converter.Converter -class SwapCurrencyConverter : TwoWayConverter { +class SwapCurrencyConverter : Converter { - override fun convert(value: Currency): CryptoCurrency { + override fun convert(value: CryptoCurrency): Currency { return when (value) { - is Currency.NonNativeToken -> { - CryptoCurrency.NonNativeToken( - id = value.id, - name = value.name, - symbol = value.symbol, - networkId = value.networkId, - contractAddress = value.contractAddress, - decimalCount = value.decimalCount, - ) - } - is Currency.NativeToken -> { - CryptoCurrency.NativeToken( - id = value.id, - name = value.name, - symbol = value.symbol, - networkId = value.networkId, - ) - } - } - } - - override fun convertBack(value: CryptoCurrency): Currency { - return when (value) { - is CryptoCurrency.NonNativeToken -> { + is CryptoCurrency.Token -> { Currency.NonNativeToken( - id = value.id, + id = value.id.value, name = value.name, symbol = value.symbol, - networkId = value.networkId, + networkId = value.network.id.value, contractAddress = value.contractAddress, - decimalCount = value.decimalCount, - logoUrl = "", + decimalCount = value.decimals, ) } - is CryptoCurrency.NativeToken -> { + is CryptoCurrency.Coin -> { Currency.NativeToken( - id = value.id, + id = value.id.value, name = value.name, symbol = value.symbol, - networkId = value.networkId, - logoUrl = "", + networkId = value.network.id.value, ) } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index bba56b76e7..73e8bf1a5d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,8 +1,11 @@ package com.tangem.feature.swap.domain.di +import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* @@ -10,6 +13,7 @@ import com.tangem.feature.swap.domain.cache.SwapDataCacheImpl import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -31,7 +35,8 @@ class SwapDomainModule { networksRepository: NetworksRepository, walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - @SwapScope getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, + @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusUseCase, + @SwapScope getCardTokensListUseCase: GetCardTokensListUseCase, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -43,14 +48,15 @@ class SwapDomainModule { networksRepository = networksRepository, walletFeatureToggles = walletFeatureToggles, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, - getCryptoCurrenciesUseCase = getCryptoCurrenciesUseCase, + getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, + getCardTokensListUseCase = getCardTokensListUseCase, ) } @Provides @Singleton fun provideBlockchainInteractor(transactionManager: TransactionManager): BlockchainInteractor { - return BlockchainInteractorImpl( + return DefaultBlockchainInteractor( transactionManager = transactionManager, ) } @@ -68,6 +74,40 @@ class SwapDomainModule { fun providesGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository) } + + @SwapScope + @Provides + @Singleton + fun providesGetCryptoCurrencyStatusUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetCryptoCurrencyStatusUseCase { + return GetCryptoCurrencyStatusUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + dispatchers = dispatchers, + ) + } + + @SwapScope + @Provides + @Singleton + fun providesGetCardTokensListUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetCardTokensListUseCase { + return GetCardTokensListUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + dispatchers = dispatchers, + ) + } } @Qualifier diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt index 7c0f6812fb..88d57a301f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.domain.models.domain +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData import com.tangem.feature.swap.domain.models.ui.TxFee @@ -15,7 +16,7 @@ import com.tangem.feature.swap.domain.models.ui.TxFee data class PermissionOptions( val approveData: RequestApproveStateData, val forTokenContractAddress: String, - val fromToken: Currency, + val fromToken: CryptoCurrency, val approveType: SwapApproveType, val txFee: TxFee, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt new file mode 100644 index 0000000000..e4909bfb51 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.swap.domain.models.ui + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.swap.domain.models.domain.SwapPair + +data class TokensDataStateExpress( + val initialCryptoCurrency: CryptoCurrency, + val preselectTokens: PreselectTokensExpress, + val foundTokensState: FoundTokensStateExpress, + val pairs: List, +) + +data class FoundTokensStateExpress( + val tokensInWallet: List, + val loadedTokens: List, +) + +data class PreselectTokensExpress( + val fromToken: CryptoCurrency, + val toToken: CryptoCurrency, +) + +data class TokenWithBalanceExpress( + val token: CryptoCurrency, + val tokenBalanceData: TokenBalanceDataExpress? = null, +) + +data class TokenBalanceDataExpress( + val amount: String?, + val amountEquivalent: String?, +) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index ef3b9c7ac3..bb52e2b01d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -2,10 +2,10 @@ package com.tangem.feature.swap.converters import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.TokenIconState -import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.NetworkInfo -import com.tangem.feature.swap.domain.models.ui.FoundTokensState -import com.tangem.feature.swap.domain.models.ui.TokenWithBalance +import com.tangem.feature.swap.domain.models.ui.FoundTokensStateExpress +import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress import com.tangem.feature.swap.models.Network import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData @@ -18,7 +18,7 @@ class TokensDataConverter( private val isBalanceHiddenProvider: Provider, ) { - fun convertWithNetwork(value: FoundTokensState, network: NetworkInfo): SwapSelectTokenStateHolder { + fun convertWithNetwork(value: FoundTokensStateExpress, network: NetworkInfo): SwapSelectTokenStateHolder { return SwapSelectTokenStateHolder( availableTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), unavailableTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), @@ -28,12 +28,14 @@ class TokensDataConverter( ) } - private fun tokenWithBalanceToTokenToSelect(tokenWithBalance: TokenWithBalance): TokenToSelectState.TokenToSelect { + private fun tokenWithBalanceToTokenToSelect( + tokenWithBalance: TokenWithBalanceExpress, + ): TokenToSelectState.TokenToSelect { return TokenToSelectState.TokenToSelect( - id = tokenWithBalance.token.id, + id = tokenWithBalance.token.id.value, name = tokenWithBalance.token.name, symbol = tokenWithBalance.token.symbol, - isNative = tokenWithBalance.token is Currency.NativeToken, + isNative = tokenWithBalance.token is CryptoCurrency.Coin, // todo replace converting tokenIcon = TokenIconState.CoinIcon( url = "", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 1fc4583726..31688a00c0 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -37,7 +37,7 @@ data class SwapCardData( val amountEquivalent: String?, val coinId: String?, val amountTextFieldValue: TextFieldValue?, - val tokenIconUrl: String, + val tokenIconUrl: String?, val tokenCurrency: String, val balance: String, val isBalanceHidden: Boolean, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index af9fc07c61..9f591cd1c8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -6,6 +6,7 @@ import com.tangem.common.Provider import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.domain.Currency @@ -73,21 +74,21 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: fun createQuotesLoadingState( uiStateHolder: SwapStateHolder, - fromToken: Currency, - toToken: Currency, + fromToken: CryptoCurrency, + toToken: CryptoCurrency, mainTokenId: String, ): SwapStateHolder { - val canSelectSendToken = mainTokenId != fromToken.id - val canSelectReceiveToken = mainTokenId != toToken.id + val canSelectSendToken = mainTokenId != fromToken.id.value // TODO look at id matching + val canSelectReceiveToken = mainTokenId != toToken.id.value // TODO look at id matching return uiStateHolder.copy( sendCardData = SwapCardData( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = null, - tokenIconUrl = fromToken.logoUrl, + tokenIconUrl = fromToken.iconUrl, tokenCurrency = fromToken.symbol, - coinId = fromToken.id, - isNotNativeToken = fromToken.isNonNative(), + coinId = fromToken.id.value, + isNotNativeToken = fromToken is CryptoCurrency.Token, canSelectAnotherToken = canSelectSendToken, balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "", isBalanceHidden = isBalanceHiddenProvider(), @@ -96,10 +97,10 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: type = TransactionCardType.ReceiveCard(), amountTextFieldValue = null, amountEquivalent = null, - tokenIconUrl = toToken.logoUrl, + tokenIconUrl = toToken.iconUrl, tokenCurrency = toToken.symbol, - coinId = toToken.id, - isNotNativeToken = toToken.isNonNative(), + coinId = toToken.id.value, + isNotNativeToken = toToken is CryptoCurrency.Token, canSelectAnotherToken = canSelectReceiveToken, balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "", isBalanceHidden = isBalanceHiddenProvider(), @@ -123,7 +124,7 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: fun createQuotesLoadedState( uiStateHolder: SwapStateHolder, quoteModel: SwapState.QuotesLoadedState, - fromToken: Currency, + fromToken: CryptoCurrency, onFeeSetup: (TxFee) -> Unit, ): SwapStateHolder { val warnings = mutableListOf() @@ -238,7 +239,7 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: fun addTokensToState( uiState: SwapStateHolder, - dataState: FoundTokensState, + dataState: FoundTokensStateExpress, networkInfo: NetworkInfo, ): SwapStateHolder { return uiState.copy( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 703f7bb82c..b3c9b23c76 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -151,7 +151,7 @@ private fun MainInfo(state: SwapStateHolder) { }, textFieldValue = state.sendCardData.amountTextFieldValue, amountEquivalent = state.sendCardData.amountEquivalent, - tokenIconUrl = state.sendCardData.tokenIconUrl, + tokenIconUrl = state.sendCardData.tokenIconUrl ?: "", tokenCurrency = state.sendCardData.tokenCurrency, priceImpact = priceImpactWarning, networkIconRes = if (state.sendCardData.isNotNativeToken) networkIconRes else null, @@ -169,7 +169,7 @@ private fun MainInfo(state: SwapStateHolder) { balance = if (state.receiveCardData.isBalanceHidden) STARS else state.receiveCardData.balance, textFieldValue = state.receiveCardData.amountTextFieldValue, amountEquivalent = state.receiveCardData.amountEquivalent, - tokenIconUrl = state.receiveCardData.tokenIconUrl, + tokenIconUrl = state.receiveCardData.tokenIconUrl ?: "", tokenCurrency = state.receiveCardData.tokenCurrency, priceImpact = priceImpactWarning, networkIconRes = if (state.receiveCardData.isNotNativeToken) networkIconRes else null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 71c151673d..1da348c56c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -8,6 +8,7 @@ import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor @@ -51,10 +52,14 @@ internal class SwapViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { + // try to get rid of this and use only CryptoCurrency private val currency = Json.decodeFromString( savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] ?: error("no expected parameter Currency found"), ) + + private var cryptoCurrency: CryptoCurrency by Delegates.notNull() + private val derivationPath = savedStateHandle.get(SwapFragment.DERIVATION_PATH) private val network = savedStateHandle.get(SwapFragment.NETWORK) @@ -132,8 +137,21 @@ internal class SwapViewModel @Inject constructor( // new flow viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { - val pairs = swapInteractor.getPairs(currency) - // TODO + swapInteractor.getTokensDataState(currency) + }.onSuccess { state -> + dataState = dataState.copy( + fromCryptoCurrency = state.preselectTokens.fromToken, + toCryptoCurrency = state.preselectTokens.toToken, + ) + cryptoCurrency = state.initialCryptoCurrency + // updateTokensState(dataState = state.foundTokensState) + // startLoadingQuotes( + // fromToken = state.preselectTokens.fromToken, + // toToken = state.preselectTokens.toToken, + // amount = lastAmount.value, + // ) + }.onFailure { + Timber.tag(loggingTag).e(it) } } @@ -143,24 +161,24 @@ internal class SwapViewModel @Inject constructor( swapInteractor.initTokensToSwap(currency) } .onSuccess { state -> - dataState = dataState.copy( - fromCurrency = state.preselectTokens.fromToken, - toCurrency = state.preselectTokens.toToken, - ) - updateTokensState(dataState = state.foundTokensState) - startLoadingQuotes( - fromToken = state.preselectTokens.fromToken, - toToken = state.preselectTokens.toToken, - amount = lastAmount.value, - ) + // dataState = dataState.copy( + // fromCurrency = state.preselectTokens.fromToken, + // toCurrency = state.preselectTokens.toToken, + // ) + // updateTokensState(dataState = state.foundTokensState) + // startLoadingQuotes( + // fromToken = state.preselectTokens.fromToken, + // toToken = state.preselectTokens.toToken, + // amount = lastAmount.value, + // ) } .onFailure { - Timber.e(it) + Timber.tag(loggingTag).e(it) } } } - private fun updateTokensState(dataState: FoundTokensState) { + private fun updateTokensState(dataState: FoundTokensStateExpress) { uiState = stateBuilder.addTokensToState( uiState = uiState, dataState = dataState, @@ -168,9 +186,9 @@ internal class SwapViewModel @Inject constructor( ) } - private fun startLoadingQuotes(fromToken: Currency, toToken: Currency, amount: String) { + private fun startLoadingQuotes(fromToken: CryptoCurrency, toToken: CryptoCurrency, amount: String) { singleTaskScheduler.cancelTask() - uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, currency.id) + uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, cryptoCurrency.id.value) singleTaskScheduler.scheduleTask( viewModelScope, loadQuotesTask( @@ -182,15 +200,19 @@ internal class SwapViewModel @Inject constructor( } private fun startLoadingQuotesFromLastState() { - val fromCurrency = dataState.fromCurrency - val toCurrency = dataState.toCurrency + val fromCurrency = dataState.fromCryptoCurrency + val toCurrency = dataState.toCryptoCurrency val amount = dataState.amount if (fromCurrency != null && toCurrency != null && amount != null) { startLoadingQuotes(fromCurrency, toCurrency, amount) } } - private fun loadQuotesTask(fromToken: Currency, toToken: Currency, amount: String): PeriodicTask { + private fun loadQuotesTask( + fromToken: CryptoCurrency, + toToken: CryptoCurrency, + amount: String, + ): PeriodicTask { return PeriodicTask( UPDATE_DELAY, task = { @@ -264,8 +286,8 @@ internal class SwapViewModel @Inject constructor( swapInteractor.onSwap( networkId = dataState.networkId, swapStateData = requireNotNull(dataState.swapDataModel), - currencyToSend = requireNotNull(dataState.fromCurrency), - currencyToGet = requireNotNull(dataState.toCurrency), + currencyToSend = requireNotNull(dataState.fromCryptoCurrency), + currencyToGet = requireNotNull(dataState.toCryptoCurrency), amountToSwap = requireNotNull(dataState.amount), fee = requireNotNull(dataState.selectedFee), ) @@ -314,9 +336,9 @@ internal class SwapViewModel @Inject constructor( approveData = requireNotNull(dataState.approveDataModel) { "dataState.approveDataModel might not be null" }, - forTokenContractAddress = (dataState.fromCurrency as? Currency.NonNativeToken)?.contractAddress + forTokenContractAddress = (dataState.fromCryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: "", - fromToken = requireNotNull(dataState.fromCurrency) { + fromToken = requireNotNull(dataState.fromCryptoCurrency) { "dataState.fromCurrency might not be null" }, approveType = requireNotNull(uiState.permissionState as? SwapPermissionState.ReadyForRequest) { @@ -367,18 +389,18 @@ internal class SwapViewModel @Inject constructor( ) if (foundToken != null) { - val fromToken: Currency - val toToken: Currency + val fromToken: CryptoCurrency + val toToken: CryptoCurrency if (isOrderReversed) { fromToken = foundToken - toToken = currency + toToken = cryptoCurrency } else { - fromToken = currency + fromToken = cryptoCurrency toToken = foundToken } dataState = dataState.copy( - fromCurrency = fromToken, - toCurrency = toToken, + fromCryptoCurrency = fromToken, + toCryptoCurrency = toToken, ) startLoadingQuotes(fromToken, toToken, lastAmount.value) swapRouter.openScreen(SwapNavScreen.Main) @@ -386,12 +408,12 @@ internal class SwapViewModel @Inject constructor( } private fun onChangeCardsClicked() { - val newFromToken = dataState.toCurrency - val newToToken = dataState.fromCurrency + val newFromToken = dataState.toCryptoCurrency + val newToToken = dataState.fromCryptoCurrency if (newFromToken != null && newToToken != null) { dataState = dataState.copy( - fromCurrency = newFromToken, - toCurrency = newToToken, + fromCryptoCurrency = newFromToken, + toCryptoCurrency = newToToken, ) isOrderReversed = !isOrderReversed val decimals = blockchainInteractor.getTokenDecimals(newFromToken) @@ -405,8 +427,8 @@ internal class SwapViewModel @Inject constructor( } private fun onAmountChanged(value: String) { - val fromToken = dataState.fromCurrency - val toToken = dataState.toCurrency + val fromToken = dataState.fromCryptoCurrency + val toToken = dataState.toCryptoCurrency if (fromToken != null && toToken != null) { val decimals = blockchainInteractor.getTokenDecimals(fromToken) val cutValue = cutAmountWithDecimals(decimals, value) @@ -420,8 +442,8 @@ internal class SwapViewModel @Inject constructor( } private fun onMaxAmountClicked() { - dataState.fromCurrency?.let { - val balance = swapInteractor.getTokenBalance(currency.networkId, it) + dataState.fromCryptoCurrency?.let { + val balance = swapInteractor.getTokenBalance(cryptoCurrency.network.id.value, it) onAmountChanged(balance.formatToUIRepresentation()) } } @@ -496,11 +518,11 @@ internal class SwapViewModel @Inject constructor( onSelectItemFee = { feeItem -> dataState = dataState.copy(selectedFee = feeItem.data) val spendAmount = dataState.amount?.let { amount -> - val fromToken = dataState.fromCurrency ?: return@let null + val fromToken = dataState.fromCryptoCurrency ?: return@let null swapInteractor.getSwapAmountForToken(amount, fromToken) } ?: dataState.approveDataModel?.fromTokenAmount spendAmount ?: return@UiActions - val fromToken = dataState.fromCurrency ?: return@UiActions + val fromToken = dataState.fromCryptoCurrency ?: return@UiActions viewModelScope.launch(dispatchers.io) { val isFeeEnough = swapInteractor.checkFeeIsEnough( fee = feeItem.data.feeValue, @@ -517,6 +539,7 @@ internal class SwapViewModel @Inject constructor( } companion object { + private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" private const val UPDATE_DELAY = 10000L private const val DEBOUNCE_AMOUNT_DELAY = 1000L From 6fba280ec33e3ee6989e957d0a508a56eec67441 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Nov 2023 14:34:52 +0200 Subject: [PATCH 019/139] Updated on 2026-08-14 --- .../tangem/feature/swap/SwapRepositoryImpl.kt | 33 ------------------- .../swap/domain/di/SwapDomainModule.kt | 2 -- 2 files changed, 35 deletions(-) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 514ea79360..35cb083d21 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -191,39 +191,6 @@ internal class SwapRepositoryImpl @Inject constructor( return configManager.config.swapReferrerAccount?.fee?.toDoubleOrNull() ?: 0.0 } - override suspend fun getCryptoCurrency( - userWallet: UserWallet, - currency: CryptoCurrency, - network: Network, - ): CryptoCurrency? { - val blockchain = Blockchain.fromNetworkId(currency.network.id.value) ?: return null - val cryptoCurrencyFactory = CryptoCurrencyFactory() - return when (currency) { - is CryptoCurrency.Coin -> { - cryptoCurrencyFactory.createCoin( - blockchain = blockchain, - extraDerivationPath = network.derivationPath.value, - derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, - ) - } - is CryptoCurrency.Token -> { - val sdkToken = SdkToken( - name = currency.name, - symbol = currency.symbol, - contractAddress = currency.contractAddress, - decimals = currency.decimals, - id = currency.id.value, - ) - cryptoCurrencyFactory.createToken( - sdkToken = sdkToken, - blockchain = blockchain, - extraDerivationPath = network.derivationPath.value, - derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, - ) - } - } as CryptoCurrency - } - override suspend fun getAllowance( userWalletId: UserWalletId, networkId: String, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 73e8bf1a5d..dc882859ae 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -36,7 +36,6 @@ class SwapDomainModule { walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusUseCase, - @SwapScope getCardTokensListUseCase: GetCardTokensListUseCase, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -49,7 +48,6 @@ class SwapDomainModule { walletFeatureToggles = walletFeatureToggles, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, - getCardTokensListUseCase = getCardTokensListUseCase, ) } From b2291e9864fac09716609ed7a853a8e6036a0ed0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Nov 2023 15:50:21 +0300 Subject: [PATCH 020/139] Updated on 2026-08-14 --- .../tokens/GetCryptoCurrencyStatusUseCase.kt | 1 + .../tangem/feature/swap/SwapRepositoryImpl.kt | 5 -- .../feature/swap/domain/SwapInteractorImpl.kt | 1 + .../feature/swap/models/SwapStateHolder.kt | 6 ++- .../tangem/feature/swap/ui/StateBuilder.kt | 35 +++++++++++- .../feature/swap/ui/SwapScreenContent.kt | 53 ++++++++++--------- .../feature/swap/viewmodels/SwapViewModel.kt | 3 +- 7 files changed, 69 insertions(+), 35 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt index 51029ebe6e..d917a747c7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt @@ -35,6 +35,7 @@ class GetCryptoCurrencyStatusUseCase( } } + @Suppress("UnusedPrivateMember") private fun createTokenList( userWalletId: UserWalletId, tokens: List, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 35cb083d21..ce4a54b96e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.common.Approver import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.extensions.Result -import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.request.PairsRequestBody @@ -16,11 +15,8 @@ import com.tangem.datasource.api.oneinch.errors.OneIncResponseException import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.config.ConfigManager import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.SwapRepository @@ -32,7 +28,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.withContext import java.math.BigDecimal import javax.inject.Inject -import com.tangem.blockchain.common.Token as SdkToken import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo @Suppress("LongParameterList") diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 1381d354c9..ada40cc077 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -453,6 +453,7 @@ internal class SwapInteractorImpl @Inject constructor( return toToken } + @Suppress("UnusedPrivateMember") @Deprecated("used in old swap mechanism") private fun getTokensWithBalance( tokens: List, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 31688a00c0..ace9ee54d5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.models import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.feature.swap.domain.models.ui.TxFee @@ -87,8 +88,9 @@ sealed interface TransactionCardType { } sealed interface SwapWarning { - data class PermissionNeeded(val tokenCurrency: String) : SwapWarning + data class PermissionNeeded(val notificationConfig: NotificationConfig) : SwapWarning object InsufficientFunds : SwapWarning + data class NoAvailableTokensToSwap(val notificationConfig: NotificationConfig) : SwapWarning data class GenericWarning( val message: String? = null, val type: GenericWarningType = GenericWarningType.OTHER, @@ -101,7 +103,7 @@ sealed interface SwapWarning { * * @property priceImpact in format = 10 (means 10%) */ - data class HighPriceImpact(val priceImpact: Int) : SwapWarning + data class HighPriceImpact(val priceImpact: Int, val notificationConfig: NotificationConfig) : SwapWarning } enum class GenericWarningType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 9f591cd1c8..0fc01468a0 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -3,9 +3,12 @@ package com.tangem.feature.swap.ui import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.Provider +import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError @@ -132,14 +135,23 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: quoteModel.preparedSwapConfigState.isFeeEnough && quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest ) { - warnings.add(SwapWarning.PermissionNeeded(fromToken.symbol)) + warnings.add( + SwapWarning.PermissionNeeded( + createPermissionNotificationConfig(fromToken.symbol), + ), + ) } if (!quoteModel.preparedSwapConfigState.isBalanceEnough) { warnings.add(SwapWarning.InsufficientFunds) } if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) { - warnings.add(SwapWarning.HighPriceImpact((quoteModel.priceImpact * HUNDRED_PERCENTS).toInt())) + warnings.add( + SwapWarning.HighPriceImpact( + priceImpact = (quoteModel.priceImpact * HUNDRED_PERCENTS).toInt(), + notificationConfig = highPriceImpactNotificationConfig(), + ), + ) } val feeState = createFeeState(quoteModel, uiStateHolder, onFeeSetup) return uiStateHolder.copy( @@ -626,6 +638,25 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: } } + private fun createPermissionNotificationConfig(fromTokenSymbol: String): NotificationConfig { + return NotificationConfig( + title = resourceReference(R.string.swapping_permission_header), + subtitle = resourceReference( + id = R.string.swapping_permission_subheader, + formatArgs = wrappedList(fromTokenSymbol), + ), + iconResId = R.drawable.ic_locked_24, + ) + } + + private fun highPriceImpactNotificationConfig(): NotificationConfig { + return NotificationConfig( + title = resourceReference(R.string.swapping_high_price_impact), + subtitle = resourceReference(R.string.swapping_high_price_impact_description), + iconResId = R.drawable.ic_locked_24, + ) + } + private fun getShortAddressValue(fullAddress: String): String { check(fullAddress.length > ADDRESS_MIN_LENGTH) { "Invalid address" } val firstAddressPart = fullAddress.substring(startIndex = 0, endIndex = ADDRESS_FIRST_PART_LENGTH) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index b3c9b23c76..1ea66c1e90 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -20,11 +20,14 @@ import androidx.constraintlayout.compose.ConstraintLayout import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.extensions.getActiveIconResByCoinId +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.domain.models.ui.TxFee @@ -280,26 +283,13 @@ private fun SwapWarnings(warnings: List) { warnings.forEach { warning -> when (warning) { is SwapWarning.HighPriceImpact -> { - WarningCard( - title = stringResource(id = R.string.swapping_high_price_impact), - description = stringResource(id = R.string.swapping_high_price_impact_description), + Notification( + config = warning.notificationConfig, ) } is SwapWarning.PermissionNeeded -> { - WarningCard( - title = stringResource(id = R.string.swapping_permission_header), - description = stringResource( - id = R.string.swapping_permission_subheader, - warning.tokenCurrency, - ), - icon = { - Icon( - painter = painterResource(id = com.tangem.core.ui.R.drawable.ic_locked_24), - contentDescription = null, - modifier = Modifier.size(TangemTheme.dimens.size20), - tint = TangemTheme.colors.icon.primary1, - ) - }, + Notification( + config = warning.notificationConfig, ) } is SwapWarning.GenericWarning -> { @@ -316,14 +306,12 @@ private fun SwapWarnings(warnings: List) { onClick = warning.onClick, ) } + is SwapWarning.NoAvailableTokensToSwap -> { + Notification( + config = warning.notificationConfig, + ) + } else -> {} - // is SwapWarning.RateExpired -> { - // RefreshableWaringCard( - // title = stringResource(id = R.string.), - // description = stringResource(id = R.string.), - // onClick = warning.onClick, - // ) - // } } SpacerH8() } @@ -446,7 +434,22 @@ private val state = SwapStateHolder( state = stateSelectable, onSelectItem = {}, ), - warnings = listOf(SwapWarning.PermissionNeeded("DAI")), + warnings = listOf( + SwapWarning.PermissionNeeded( + notificationConfig = NotificationConfig( + title = stringReference("Give Premission"), + subtitle = stringReference("To continue swapping you need to give permission to Tangem"), + iconResId = R.drawable.ic_locked_24, + ), + ), + SwapWarning.NoAvailableTokensToSwap( + notificationConfig = NotificationConfig( + title = stringReference("No tokens"), + subtitle = stringReference("Swap tokens not available"), + iconResId = R.drawable.ic_alert_24, + ), + ), + ), networkCurrency = "MATIC", swapButton = SwapButton(enabled = true, loading = false, onClick = {}), onRefresh = {}, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 1da348c56c..03e7c8d26e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -336,7 +336,8 @@ internal class SwapViewModel @Inject constructor( approveData = requireNotNull(dataState.approveDataModel) { "dataState.approveDataModel might not be null" }, - forTokenContractAddress = (dataState.fromCryptoCurrency as? CryptoCurrency.Token)?.contractAddress + forTokenContractAddress = (dataState.fromCryptoCurrency as? CryptoCurrency.Token) + ?.contractAddress ?: "", fromToken = requireNotNull(dataState.fromCryptoCurrency) { "dataState.fromCurrency might not be null" From fb0cf652caff1bbcf1f99b3b0704cd2ad151b0a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Nov 2023 10:54:00 +0200 Subject: [PATCH 021/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 96 ++++++++++--------- .../models/ui/TokensDataStateExpress.kt | 17 ++-- .../feature/swap/viewmodels/SwapViewModel.kt | 6 +- 3 files changed, 59 insertions(+), 60 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index ada40cc077..93a6fc6dff 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -60,46 +60,67 @@ internal class SwapInteractorImpl @Inject constructor( requireNotNull(selectedWallet) - val currencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) + val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) .first() .getOrElse { emptyList() } - // .filter { it.currency.network.backendId != currency.networkId } - val currencies = currencyStatuses.map { it.currency } + + val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses.filter { + it.currency.network.backendId != currency.networkId || + it.currency.getContractAddress() != currency.getContractAddress() + } val pairsLeast = getPairs( initialCurrency = LeastTokenInfo( contractAddress = (currency as? Currency.NonNativeToken)?.contractAddress ?: "0", network = currency.networkId, ), - currenciesList = currencyStatuses.map { it.currency }, + currenciesList = walletCurrencyStatusesExceptInitial.map { it.currency }, ) - val pairs = createCryptoCurrencyPairs(pairsLeast, currencies) - - val initialCryptoCurrency = mapLegacyCurrencyToCryptoCurrency(currency, currencyStatuses) + val initialCryptoCurrency = mapLegacyCurrencyToCryptoCurrency(currency, walletCurrencyStatuses) ?: error("Initial crypto currency must not be null") return TokensDataStateExpress( initialCryptoCurrency = initialCryptoCurrency, - preselectTokens = getPreselectTokens(currency, currencyStatuses), - foundTokensState = FoundTokensStateExpress(emptyList(), emptyList()), - pairs = pairs, + fromGroup = getToCurrenciesGroup( + currency = initialCryptoCurrency, + leastPairs = pairsLeast, + cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, + tokenInfoForFilter = { it.from }, + tokenInfoForAvailable = { it.to } + ), + toGroup = getToCurrenciesGroup( + currency = initialCryptoCurrency, + leastPairs = pairsLeast, + cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, + tokenInfoForFilter = { it.to }, + tokenInfoForAvailable = { it.from } + ), ) } - private fun getPreselectTokens(currency: Currency, currencies: List): PreselectTokensExpress { - val from = mapLegacyCurrencyToCryptoCurrency(currency, currencies) - - val to = currencies.firstOrNull()?.currency // TODO choose of 3 variants - - if (from != null && to != null) { - return PreselectTokensExpress( - fromToken = from, - toToken = to, - ) - } else { - error("From and to currencies must not be null") + private fun getToCurrenciesGroup( + currency: CryptoCurrency, + leastPairs: List, + cryptoCurrenciesList: List, + tokenInfoForFilter: (SwapPairLeast) -> LeastTokenInfo, + tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo, + ): CurrenciesGroup { + val filteredPairs = leastPairs.filter { + tokenInfoForFilter(it).contractAddress == currency.getContractAddress() + && tokenInfoForFilter(it).network == currency.network.backendId } + + val availableCryptoCurrencies = filteredPairs.mapNotNull { + findCryptoCurrencyStatusByLeastInfo(tokenInfoForAvailable(it), cryptoCurrenciesList) + } + + val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies.toSet() + + return CurrenciesGroup( + available = availableCryptoCurrencies, + unavailable = unavailableCryptoCurrencies + ) } private fun mapLegacyCurrencyToCryptoCurrency( @@ -113,32 +134,13 @@ internal class SwapInteractorImpl @Inject constructor( } } - private fun createCryptoCurrencyPairs( - swapPairLeasts: List, - cryptoCurrenciesList: List, - ): List { - return swapPairLeasts.mapNotNull { - val from = findCryptoCurrencyByLeastInfo(it.from, cryptoCurrenciesList) - val to = findCryptoCurrencyByLeastInfo(it.to, cryptoCurrenciesList) - if (from != null && to != null) { - SwapPair( - from = from, - to = to, - providers = it.providers, - ) - } else { - null - } - } - } - - private fun findCryptoCurrencyByLeastInfo( + private fun findCryptoCurrencyStatusByLeastInfo( leastTokenInfo: LeastTokenInfo, - cryptoCurrenciesList: List, - ): CryptoCurrency? { - return cryptoCurrenciesList.find { - it.network.backendId == leastTokenInfo.network && - it.getContractAddress() == leastTokenInfo.contractAddress + cryptoCurrencyStatusesList: List, + ): CryptoCurrencyStatus? { + return cryptoCurrencyStatusesList.find { + it.currency.network.backendId == leastTokenInfo.network && + it.currency.getContractAddress() == leastTokenInfo.contractAddress } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index e4909bfb51..d4c4f67494 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -1,13 +1,17 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.feature.swap.domain.models.domain.SwapPair +import com.tangem.domain.tokens.model.CryptoCurrencyStatus data class TokensDataStateExpress( val initialCryptoCurrency: CryptoCurrency, - val preselectTokens: PreselectTokensExpress, - val foundTokensState: FoundTokensStateExpress, - val pairs: List, + val fromGroup: CurrenciesGroup, + val toGroup: CurrenciesGroup, +) + +data class CurrenciesGroup( + val available: List, + val unavailable: List, ) data class FoundTokensStateExpress( @@ -15,11 +19,6 @@ data class FoundTokensStateExpress( val loadedTokens: List, ) -data class PreselectTokensExpress( - val fromToken: CryptoCurrency, - val toToken: CryptoCurrency, -) - data class TokenWithBalanceExpress( val token: CryptoCurrency, val tokenBalanceData: TokenBalanceDataExpress? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 03e7c8d26e..119f30fea1 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -139,10 +139,8 @@ internal class SwapViewModel @Inject constructor( runCatching(dispatchers.io) { swapInteractor.getTokensDataState(currency) }.onSuccess { state -> - dataState = dataState.copy( - fromCryptoCurrency = state.preselectTokens.fromToken, - toCryptoCurrency = state.preselectTokens.toToken, - ) + // dataState = dataState.copy( + // ) cryptoCurrency = state.initialCryptoCurrency // updateTokensState(dataState = state.foundTokensState) // startLoadingQuotes( From 6b92fc6179a73b29577b3cdb7f74e7b05457aa76 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Nov 2023 14:26:13 +0200 Subject: [PATCH 022/139] Updated on 2026-08-14 --- .../swap/converters/SwapPairInfoConverter.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 10 ++++++---- .../domain/models/domain/SwapPairLeast.kt | 20 ++++++------------- .../models/ui/TokensDataStateExpress.kt | 6 +++--- .../feature/swap/viewmodels/SwapViewModel.kt | 6 ++++-- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 627a7a8fb4..9889f7d698 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -5,7 +5,7 @@ import com.tangem.datasource.api.express.models.response.SwapPair import com.tangem.datasource.api.express.models.response.SwapPairProvider import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast as SwapPairDomain -import com.tangem.feature.swap.domain.models.domain.SwapPairProvider as SwapPairProviderDomain +import com.tangem.feature.swap.domain.models.domain.SwapProvider as SwapPairProviderDomain import com.tangem.feature.swap.domain.models.domain.RateType as RateTypeDomain import com.tangem.utils.converter.Converter diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 93a6fc6dff..4d0bfd3e52 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -111,15 +111,17 @@ internal class SwapInteractorImpl @Inject constructor( && tokenInfoForFilter(it).network == currency.network.backendId } - val availableCryptoCurrencies = filteredPairs.mapNotNull { - findCryptoCurrencyStatusByLeastInfo(tokenInfoForAvailable(it), cryptoCurrenciesList) + val availableCryptoCurrencies = filteredPairs.mapNotNull { pair -> + val status = findCryptoCurrencyStatusByLeastInfo(tokenInfoForAvailable(pair), cryptoCurrenciesList) + status?.let { CryptoCurrencySwapInfo(it, pair.providers) } } - val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies.toSet() + val unavailableCryptoCurrencies = + (cryptoCurrenciesList - availableCryptoCurrencies.map { it.currencyStatus }.toSet()) return CurrenciesGroup( available = availableCryptoCurrencies, - unavailable = unavailableCryptoCurrencies + unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) } ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index 704a27dc1b..a41f3d283d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.domain.models.domain -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus /** * Domain layer representation of SwapPair data network model. @@ -12,20 +12,12 @@ import com.tangem.domain.tokens.model.CryptoCurrency data class SwapPairLeast( val from: LeastTokenInfo, val to: LeastTokenInfo, - val providers: List, + val providers: List, ) -/** - * Enriched model of swap pair data. Contains full CryptoCurrency models instead of least info. - * - * @property from CryptoCurrency we want to change - * @property to CryptoCurrency we want to exchange for - * @property providers Exchange providers - */ -data class SwapPair( - val from: CryptoCurrency, - val to: CryptoCurrency, - val providers: List, +data class CryptoCurrencySwapInfo( + val currencyStatus: CryptoCurrencyStatus, + val providers: List ) /** @@ -34,7 +26,7 @@ data class SwapPair( * @property providerId provider id * @property rateTypes supported rate types */ -data class SwapPairProvider( +data class SwapProvider( val providerId: Int, val rateTypes: List, ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index d4c4f67494..8beb9ed216 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo data class TokensDataStateExpress( val initialCryptoCurrency: CryptoCurrency, @@ -10,8 +10,8 @@ data class TokensDataStateExpress( ) data class CurrenciesGroup( - val available: List, - val unavailable: List, + val available: List, + val unavailable: List, ) data class FoundTokensStateExpress( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 119f30fea1..1513b59892 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -139,8 +139,10 @@ internal class SwapViewModel @Inject constructor( runCatching(dispatchers.io) { swapInteractor.getTokensDataState(currency) }.onSuccess { state -> - // dataState = dataState.copy( - // ) + dataState = dataState.copy( + fromCryptoCurrency = state.initialCryptoCurrency, + toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency + ) cryptoCurrency = state.initialCryptoCurrency // updateTokensState(dataState = state.foundTokensState) // startLoadingQuotes( From 3539e8272aca42668de11413e45be3b9631a7698 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Nov 2023 15:53:16 +0300 Subject: [PATCH 023/139] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 72 +---------------- .../feature/swap/domain/SwapInteractor.kt | 4 +- .../feature/swap/domain/SwapInteractorImpl.kt | 41 +++------- .../models/ui/TokensDataStateExpress.kt | 1 - .../feature/swap/presentation/SwapFragment.kt | 2 - .../tangem/feature/swap/ui/StateBuilder.kt | 12 ++- .../feature/swap/viewmodels/SwapViewModel.kt | 80 +++++++++---------- 7 files changed, 60 insertions(+), 152 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index c3092a3714..f03fc2cd20 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -9,11 +9,8 @@ import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.common.extensions.toCoinId -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.features.send.api.navigation.SendRouter import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -25,7 +22,6 @@ import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.tokens.getIconUrl import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.send.redux.PrepareSendScreen @@ -42,7 +38,6 @@ import com.tangem.tap.store import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency @Suppress("LargeClass") class TradeCryptoMiddleware { @@ -57,17 +52,12 @@ class TradeCryptoMiddleware { is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action) is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) is TradeCryptoAction.Swap -> { - openSwap( - currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency(), - derivationPath = store.state.walletState.selectedWalletData?.currency?.derivationPath, - ) + // todo remove old flow } is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action) is TradeCryptoAction.New.Sell -> proceedNewSellAction(action) is TradeCryptoAction.New.Swap -> openSwap( - currency = action.cryptoCurrency.toSwapCurrency(), - derivationPath = action.cryptoCurrency.network.derivationPath.value, - network = action.cryptoCurrency.network, + currency = action.cryptoCurrency, ) is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action) is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action) @@ -267,70 +257,14 @@ class TradeCryptoMiddleware { )?.let { store.dispatchOpenUrl(it) } } - private fun openSwap(currency: SwapCurrency?, derivationPath: String?, network: Network? = null) { + private fun openSwap(currency: CryptoCurrency) { val bundle = bundleOf( SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency), - SwapFragment.DERIVATION_PATH to derivationPath, - SwapFragment.NETWORK to network, ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) } - private fun CryptoCurrency.toSwapCurrency(): SwapCurrency { - val blockchain = Blockchain.fromId(network.id.value) - - return when (this) { - is CryptoCurrency.Coin -> { - SwapCurrency.NativeToken( - id = blockchain.toCoinId(), - name = name, - symbol = symbol, - networkId = blockchain.toNetworkId(), - // no need to set logoUrl for blockchain cause - // error when form url with coinId, coinId of eth and arbitrum the same - logoUrl = "", - ) - } - is CryptoCurrency.Token -> { - SwapCurrency.NonNativeToken( - id = id.rawCurrencyId ?: "", - name = name, - symbol = symbol, - networkId = blockchain.toNetworkId(), - logoUrl = getIconUrl(id.rawCurrencyId ?: ""), - contractAddress = contractAddress, - decimalCount = decimals, - ) - } - } - } - - private fun Currency.toSwapCurrency(): SwapCurrency { - return when (this) { - is Currency.Blockchain -> { - SwapCurrency.NativeToken( - id = blockchain.toCoinId(), - name = this.currencyName, - symbol = this.currencySymbol, - networkId = this.blockchain.toNetworkId(), - // no need to set logoUrl for blockchain cause - // error when form url with coinId, coinId of eth and arbitrum the same - logoUrl = "", - ) - } - is Currency.Token -> SwapCurrency.NonNativeToken( - id = this.token.id ?: "", - name = this.currencyName, - symbol = this.currencySymbol, - networkId = this.blockchain.toNetworkId(), - logoUrl = getIconUrl(this.token.id ?: ""), - contractAddress = this.token.contractAddress, - decimalCount = decimals, - ) - } - } - private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) { val currency = action.tokenCurrency val blockchain = Blockchain.fromId(currency.network.id.value) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 13d3c2d3f4..2757583249 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -9,9 +9,9 @@ import java.math.BigDecimal interface SwapInteractor { - suspend fun getTokensDataState(currency: Currency): TokensDataStateExpress + suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress - fun initDerivationPathAndNetwork(derivationPath: String?, network: Network?) + fun initDerivationPathAndNetwork(derivationPath: String?, network: Network) /** * Init tokens to swap, load tokens list available to swap for given network diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 93a6fc6dff..3e63caec40 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import arrow.core.getOrElse +import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -52,7 +52,7 @@ internal class SwapInteractorImpl @Inject constructor( private var derivationPath: String? = null private var network: Network? = null - override suspend fun getTokensDataState(currency: Currency): TokensDataStateExpress { + override suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { val selectedWallet = getSelectedWalletSyncUseCase().fold( ifLeft = { null }, ifRight = { it }, @@ -65,36 +65,32 @@ internal class SwapInteractorImpl @Inject constructor( .getOrElse { emptyList() } val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses.filter { - it.currency.network.backendId != currency.networkId || + it.currency.network.backendId != currency.network.backendId || it.currency.getContractAddress() != currency.getContractAddress() } val pairsLeast = getPairs( initialCurrency = LeastTokenInfo( - contractAddress = (currency as? Currency.NonNativeToken)?.contractAddress ?: "0", - network = currency.networkId, + contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", + network = currency.network.backendId, ), currenciesList = walletCurrencyStatusesExceptInitial.map { it.currency }, ) - val initialCryptoCurrency = mapLegacyCurrencyToCryptoCurrency(currency, walletCurrencyStatuses) - ?: error("Initial crypto currency must not be null") - return TokensDataStateExpress( - initialCryptoCurrency = initialCryptoCurrency, fromGroup = getToCurrenciesGroup( - currency = initialCryptoCurrency, + currency = currency, leastPairs = pairsLeast, cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, tokenInfoForFilter = { it.from }, - tokenInfoForAvailable = { it.to } + tokenInfoForAvailable = { it.to }, ), toGroup = getToCurrenciesGroup( - currency = initialCryptoCurrency, + currency = currency, leastPairs = pairsLeast, cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, tokenInfoForFilter = { it.to }, - tokenInfoForAvailable = { it.from } + tokenInfoForAvailable = { it.from }, ), ) } @@ -107,8 +103,8 @@ internal class SwapInteractorImpl @Inject constructor( tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo, ): CurrenciesGroup { val filteredPairs = leastPairs.filter { - tokenInfoForFilter(it).contractAddress == currency.getContractAddress() - && tokenInfoForFilter(it).network == currency.network.backendId + tokenInfoForFilter(it).contractAddress == currency.getContractAddress() && + tokenInfoForFilter(it).network == currency.network.backendId } val availableCryptoCurrencies = filteredPairs.mapNotNull { @@ -119,21 +115,10 @@ internal class SwapInteractorImpl @Inject constructor( return CurrenciesGroup( available = availableCryptoCurrencies, - unavailable = unavailableCryptoCurrencies + unavailable = unavailableCryptoCurrencies, ) } - private fun mapLegacyCurrencyToCryptoCurrency( - currency: Currency, - currencies: List, - ): CryptoCurrency? { - return currencies.map { it.currency } - .find { - it.network.backendId == currency.networkId && - it.getContractAddress() == currency.getContractAddress() - } - } - private fun findCryptoCurrencyStatusByLeastInfo( leastTokenInfo: LeastTokenInfo, cryptoCurrencyStatusesList: List, @@ -163,7 +148,7 @@ internal class SwapInteractorImpl @Inject constructor( } @Deprecated("used in old swap mechanism") - override fun initDerivationPathAndNetwork(derivationPath: String?, network: Network?) { + override fun initDerivationPathAndNetwork(derivationPath: String?, network: Network) { this.derivationPath = derivationPath this.network = network } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index d4c4f67494..2401c08574 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -4,7 +4,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus data class TokensDataStateExpress( - val initialCryptoCurrency: CryptoCurrency, val fromGroup: CurrenciesGroup, val toGroup: CurrenciesGroup, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 8562c1e5db..00744ef60a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -82,7 +82,5 @@ class SwapFragment : ComposeFragment() { companion object { const val CURRENCY_BUNDLE_KEY = "swap_currency" - const val DERIVATION_PATH = "DERIVATION_STYLE" - const val NETWORK = "NETWORK" } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 0fc01468a0..bea53175af 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -12,9 +12,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError -import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.domain.NetworkInfo -import com.tangem.feature.swap.domain.models.domain.isNonNative import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* @@ -34,19 +32,19 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: isBalanceHiddenProvider = isBalanceHiddenProvider, ) - fun createInitialLoadingState(initialCurrency: Currency, networkInfo: NetworkInfo): SwapStateHolder { + fun createInitialLoadingState(initialCurrency: CryptoCurrency, networkInfo: NetworkInfo): SwapStateHolder { return SwapStateHolder( - networkId = initialCurrency.networkId, + networkId = initialCurrency.network.backendId, blockchainId = networkInfo.blockchainId, sendCardData = SwapCardData( type = TransactionCardType.SendCard(actions.onAmountChanged, actions.onAmountSelected), amountEquivalent = null, amountTextFieldValue = null, - tokenIconUrl = initialCurrency.logoUrl, + tokenIconUrl = initialCurrency.iconUrl, tokenCurrency = initialCurrency.symbol, - coinId = initialCurrency.id, + coinId = null, canSelectAnotherToken = false, - isNotNativeToken = initialCurrency.isNonNative(), + isNotNativeToken = initialCurrency is CryptoCurrency.Token, balance = "", isBalanceHidden = true, ), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 119f30fea1..b4b434e00e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -9,11 +9,9 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor -import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* @@ -52,17 +50,11 @@ internal class SwapViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { - // try to get rid of this and use only CryptoCurrency - private val currency = Json.decodeFromString( + private val initialCryptoCurrency = Json.decodeFromString( savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] ?: error("no expected parameter Currency found"), ) - private var cryptoCurrency: CryptoCurrency by Delegates.notNull() - - private val derivationPath = savedStateHandle.get(SwapFragment.DERIVATION_PATH) - private val network = savedStateHandle.get(SwapFragment.NETWORK) - private var isBalanceHidden = true private val stateBuilder = StateBuilder( @@ -75,12 +67,12 @@ internal class SwapViewModel @Inject constructor( private val amountDebouncer = Debouncer() private val singleTaskScheduler = SingleTaskScheduler() - private var dataState by mutableStateOf(SwapProcessDataState(networkId = currency.networkId)) + private var dataState by mutableStateOf(SwapProcessDataState(networkId = initialCryptoCurrency.network.backendId)) var uiState: SwapStateHolder by mutableStateOf( stateBuilder.createInitialLoadingState( - initialCurrency = currency, - networkInfo = blockchainInteractor.getBlockchainInfo(currency.networkId), + initialCurrency = initialCryptoCurrency, + networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId), ), ) private set @@ -93,8 +85,11 @@ internal class SwapViewModel @Inject constructor( get() = swapRouter.currentScreen init { - swapInteractor.initDerivationPathAndNetwork(derivationPath, network) - initTokens(currency) + swapInteractor.initDerivationPathAndNetwork( + derivationPath = initialCryptoCurrency.network.derivationPath.value, + network = initialCryptoCurrency.network, + ) + initTokens(initialCryptoCurrency) } override fun onCreate(owner: LifecycleOwner) { @@ -115,7 +110,7 @@ internal class SwapViewModel @Inject constructor( } fun onScreenOpened() { - analyticsEventHandler.send(SwapEvents.SwapScreenOpened(currency.symbol)) + analyticsEventHandler.send(SwapEvents.SwapScreenOpened(initialCryptoCurrency.symbol)) } fun setRouter(router: SwapRouter) { @@ -133,15 +128,14 @@ internal class SwapViewModel @Inject constructor( } @Suppress("UnusedPrivateMember") - private fun initTokens(currency: Currency) { + private fun initTokens(currency: CryptoCurrency) { // new flow viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { - swapInteractor.getTokensDataState(currency) + swapInteractor.getTokensDataState(initialCryptoCurrency) }.onSuccess { state -> // dataState = dataState.copy( // ) - cryptoCurrency = state.initialCryptoCurrency // updateTokensState(dataState = state.foundTokensState) // startLoadingQuotes( // fromToken = state.preselectTokens.fromToken, @@ -154,39 +148,39 @@ internal class SwapViewModel @Inject constructor( } // old flow - viewModelScope.launch(dispatchers.main) { - runCatching(dispatchers.io) { - swapInteractor.initTokensToSwap(currency) - } - .onSuccess { state -> - // dataState = dataState.copy( - // fromCurrency = state.preselectTokens.fromToken, - // toCurrency = state.preselectTokens.toToken, - // ) - // updateTokensState(dataState = state.foundTokensState) - // startLoadingQuotes( - // fromToken = state.preselectTokens.fromToken, - // toToken = state.preselectTokens.toToken, - // amount = lastAmount.value, - // ) - } - .onFailure { - Timber.tag(loggingTag).e(it) - } - } + // viewModelScope.launch(dispatchers.main) { + // runCatching(dispatchers.io) { + // swapInteractor.initTokensToSwap(currency) + // } + // .onSuccess { state -> + // // dataState = dataState.copy( + // // fromCurrency = state.preselectTokens.fromToken, + // // toCurrency = state.preselectTokens.toToken, + // // ) + // // updateTokensState(dataState = state.foundTokensState) + // // startLoadingQuotes( + // // fromToken = state.preselectTokens.fromToken, + // // toToken = state.preselectTokens.toToken, + // // amount = lastAmount.value, + // // ) + // } + // .onFailure { + // Timber.tag(loggingTag).e(it) + // } + // } } private fun updateTokensState(dataState: FoundTokensStateExpress) { uiState = stateBuilder.addTokensToState( uiState = uiState, dataState = dataState, - networkInfo = blockchainInteractor.getBlockchainInfo(currency.networkId), + networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId), ) } private fun startLoadingQuotes(fromToken: CryptoCurrency, toToken: CryptoCurrency, amount: String) { singleTaskScheduler.cancelTask() - uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, cryptoCurrency.id.value) + uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, initialCryptoCurrency.id.value) singleTaskScheduler.scheduleTask( viewModelScope, loadQuotesTask( @@ -392,9 +386,9 @@ internal class SwapViewModel @Inject constructor( val toToken: CryptoCurrency if (isOrderReversed) { fromToken = foundToken - toToken = cryptoCurrency + toToken = initialCryptoCurrency } else { - fromToken = cryptoCurrency + fromToken = initialCryptoCurrency toToken = foundToken } dataState = dataState.copy( @@ -442,7 +436,7 @@ internal class SwapViewModel @Inject constructor( private fun onMaxAmountClicked() { dataState.fromCryptoCurrency?.let { - val balance = swapInteractor.getTokenBalance(cryptoCurrency.network.id.value, it) + val balance = swapInteractor.getTokenBalance(initialCryptoCurrency.network.id.value, it) onAmountChanged(balance.formatToUIRepresentation()) } } From b5961302a9202f0b5b76ec3e981b373383f5814d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Nov 2023 17:19:49 +0200 Subject: [PATCH 024/139] Updated on 2026-08-14 --- .../tangem/feature/swap/SwapRepositoryImpl.kt | 26 +++++++++++++++++++ .../swap/converters/RateTypeConverter.kt | 22 ++++++++++++++++ .../swap/converters/SwapPairInfoConverter.kt | 12 +++------ .../feature/swap/domain/SwapRepository.kt | 10 +++++++ .../domain/models/domain/ExchangeQuote.kt | 8 ++++++ .../feature/swap/viewmodels/SwapViewModel.kt | 11 ++++---- 6 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index ce4a54b96e..9eb5bedf15 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -46,6 +46,7 @@ internal class SwapRepositoryImpl @Inject constructor( private val swapConverter = SwapConverter() private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() + private val rateTypeConverter = RateTypeConverter() override suspend fun getPairs( initialCurrency: LeastTokenInfo, @@ -257,6 +258,31 @@ internal class SwapRepositoryImpl @Inject constructor( return oneInchApiFactory.getApi(networkId) } + override suspend fun getExchangeQuote( + fromContractAddress: String, + fromNetwork: String, + toContractAddress: String, + toNetwork: String, + fromAmount: BigDecimal, + providerId: Int, + rateType: RateType + ): ExchangeQuote { + val response = tangemExpressApi.getExchangeQuote( + fromContractAddress, + fromNetwork, + toContractAddress, + toNetwork, + fromAmount, + providerId, + rateTypeConverter.convertBack(rateType) + ).getOrThrow() + + return ExchangeQuote( + toAmount = response.toAmount, + allowanceContract = response.allowanceContract + ) + } + companion object { // TODO("get this ids from blockchain enum later") private const val OPTIMISM_ID = "optimistic-ethereum" diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt new file mode 100644 index 0000000000..0d25c4eca2 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.express.models.response.RateType +import com.tangem.utils.converter.TwoWayConverter +import com.tangem.feature.swap.domain.models.domain.RateType as RateTypeDomain + +class RateTypeConverter : TwoWayConverter { + + override fun convert(value: RateType): RateTypeDomain { + return when (value) { + RateType.FIXED -> RateTypeDomain.FIXED + RateType.FLOAT -> RateTypeDomain.FLOAT + } + } + + override fun convertBack(value: RateTypeDomain): RateType { + return when (value) { + RateTypeDomain.FIXED -> RateType.FIXED + RateTypeDomain.FLOAT -> RateType.FLOAT + } + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 9889f7d698..e1e48fc630 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -1,16 +1,16 @@ package com.tangem.feature.swap.converters -import com.tangem.datasource.api.express.models.response.RateType import com.tangem.datasource.api.express.models.response.SwapPair import com.tangem.datasource.api.express.models.response.SwapPairProvider import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast as SwapPairDomain import com.tangem.feature.swap.domain.models.domain.SwapProvider as SwapPairProviderDomain -import com.tangem.feature.swap.domain.models.domain.RateType as RateTypeDomain import com.tangem.utils.converter.Converter class SwapPairInfoConverter : Converter { + private val rateTypeConverter = RateTypeConverter() + override fun convert(value: SwapPair): SwapPairDomain { return SwapPairDomain( from = LeastTokenInfo( @@ -30,14 +30,8 @@ class SwapPairInfoConverter : Converter { private fun convertProvider(swapPairProvider: SwapPairProvider): SwapPairProviderDomain { return SwapPairProviderDomain( providerId = swapPairProvider.providerId, - rateTypes = swapPairProvider.rateTypes.map { convertRateType(it) }, + rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) }, ) } - private fun convertRateType(rateType: RateType): RateTypeDomain { - return when (rateType) { - RateType.FIXED -> RateTypeDomain.FIXED - RateType.FLOAT -> RateTypeDomain.FLOAT - } - } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index ab0e4095c8..8942e75161 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -61,4 +61,14 @@ interface SwapRepository { currency: CryptoCurrency, amount: BigDecimal?, ): String + + suspend fun getExchangeQuote( + fromContractAddress: String, + fromNetwork: String, + toContractAddress: String, + toNetwork: String, + fromAmount: BigDecimal, + providerId: Int, + rateType: RateType, + ) : ExchangeQuote } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt new file mode 100644 index 0000000000..d087b97955 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.swap.domain.models.domain + +import java.math.BigDecimal + +data class ExchangeQuote( + val toAmount: BigDecimal, + val allowanceContract: String? +) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 1513b59892..7ba34b693d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -143,13 +143,14 @@ internal class SwapViewModel @Inject constructor( fromCryptoCurrency = state.initialCryptoCurrency, toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency ) + cryptoCurrency = state.initialCryptoCurrency // updateTokensState(dataState = state.foundTokensState) - // startLoadingQuotes( - // fromToken = state.preselectTokens.fromToken, - // toToken = state.preselectTokens.toToken, - // amount = lastAmount.value, - // ) + startLoadingQuotes( + fromToken = state.initialCryptoCurrency, + toToken = state.toGroup.available.first().currencyStatus.currency, + amount = lastAmount.value, + ) }.onFailure { Timber.tag(loggingTag).e(it) } From a74ee9640c059b0261d3aaaed58d769b2ac07c3d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 17 Nov 2023 17:58:08 +0200 Subject: [PATCH 025/139] Updated on 2026-08-14 --- .../wallet/redux/middlewares/TradeCryptoMiddleware.kt | 2 +- .../com/tangem/feature/swap/viewmodels/SwapViewModel.kt | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index f03fc2cd20..66027e8a6b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -259,7 +259,7 @@ class TradeCryptoMiddleware { private fun openSwap(currency: CryptoCurrency) { val bundle = bundleOf( - SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency), + SwapFragment.CURRENCY_BUNDLE_KEY to currency ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index b4b434e00e..177f462107 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -50,10 +50,8 @@ internal class SwapViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { - private val initialCryptoCurrency = Json.decodeFromString( - savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] - ?: error("no expected parameter Currency found"), - ) + private val initialCryptoCurrency: CryptoCurrency = savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] + ?: error("no expected parameter CryptoCurrency found`") private var isBalanceHidden = true From b3df6b20b04396fea561b00ae34074dabac97769 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Nov 2023 11:17:30 +0200 Subject: [PATCH 026/139] Updated on 2026-08-14 --- .../tangem/feature/swap/domain/DefaultBlockchainInteractor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt index 0b9605485b..cb99b58076 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt @@ -27,7 +27,7 @@ internal class DefaultBlockchainInteractor @Inject constructor( return if (token is CryptoCurrency.Token) { token.decimals } else { - transactionManager.getNativeTokenDecimals(token.network.id.value) + transactionManager.getNativeTokenDecimals(token.network.backendId) } } } \ No newline at end of file From 5ad1e47e656bbded6b2bfd532672a37670666707 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Nov 2023 11:38:59 +0200 Subject: [PATCH 027/139] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 763847b303..738424d73c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -417,7 +417,7 @@ internal class SwapInteractorImpl @Inject constructor( return if (token is CryptoCurrency.Token) { token.decimals } else { - transactionManager.getNativeTokenDecimals(token.network.id.value) + transactionManager.getNativeTokenDecimals(token.network.backendId) } } From d574159e14d0e5a9f8045f6b006eb3c83e4dcb45 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Nov 2023 14:28:16 +0200 Subject: [PATCH 028/139] Updated on 2026-08-14 --- .../tangem/feature/swap/SwapRepositoryImpl.kt | 26 +++++++++++++++++++ .../swap/converters/RateTypeConverter.kt | 22 ++++++++++++++++ .../swap/converters/SwapPairInfoConverter.kt | 14 +++------- .../domain/DefaultBlockchainInteractor.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 12 +++++---- .../feature/swap/domain/SwapRepository.kt | 10 +++++++ .../domain/models/domain/ExchangeQuote.kt | 8 ++++++ .../domain/models/domain/SwapPairLeast.kt | 20 +++++--------- .../models/ui/TokensDataStateExpress.kt | 6 ++--- .../feature/swap/viewmodels/SwapViewModel.kt | 17 +++++++----- 10 files changed, 97 insertions(+), 40 deletions(-) create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index ce4a54b96e..9eb5bedf15 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -46,6 +46,7 @@ internal class SwapRepositoryImpl @Inject constructor( private val swapConverter = SwapConverter() private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() + private val rateTypeConverter = RateTypeConverter() override suspend fun getPairs( initialCurrency: LeastTokenInfo, @@ -257,6 +258,31 @@ internal class SwapRepositoryImpl @Inject constructor( return oneInchApiFactory.getApi(networkId) } + override suspend fun getExchangeQuote( + fromContractAddress: String, + fromNetwork: String, + toContractAddress: String, + toNetwork: String, + fromAmount: BigDecimal, + providerId: Int, + rateType: RateType + ): ExchangeQuote { + val response = tangemExpressApi.getExchangeQuote( + fromContractAddress, + fromNetwork, + toContractAddress, + toNetwork, + fromAmount, + providerId, + rateTypeConverter.convertBack(rateType) + ).getOrThrow() + + return ExchangeQuote( + toAmount = response.toAmount, + allowanceContract = response.allowanceContract + ) + } + companion object { // TODO("get this ids from blockchain enum later") private const val OPTIMISM_ID = "optimistic-ethereum" diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt new file mode 100644 index 0000000000..0d25c4eca2 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/RateTypeConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.express.models.response.RateType +import com.tangem.utils.converter.TwoWayConverter +import com.tangem.feature.swap.domain.models.domain.RateType as RateTypeDomain + +class RateTypeConverter : TwoWayConverter { + + override fun convert(value: RateType): RateTypeDomain { + return when (value) { + RateType.FIXED -> RateTypeDomain.FIXED + RateType.FLOAT -> RateTypeDomain.FLOAT + } + } + + override fun convertBack(value: RateTypeDomain): RateType { + return when (value) { + RateTypeDomain.FIXED -> RateType.FIXED + RateTypeDomain.FLOAT -> RateType.FLOAT + } + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 627a7a8fb4..e1e48fc630 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -1,16 +1,16 @@ package com.tangem.feature.swap.converters -import com.tangem.datasource.api.express.models.response.RateType import com.tangem.datasource.api.express.models.response.SwapPair import com.tangem.datasource.api.express.models.response.SwapPairProvider import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast as SwapPairDomain -import com.tangem.feature.swap.domain.models.domain.SwapPairProvider as SwapPairProviderDomain -import com.tangem.feature.swap.domain.models.domain.RateType as RateTypeDomain +import com.tangem.feature.swap.domain.models.domain.SwapProvider as SwapPairProviderDomain import com.tangem.utils.converter.Converter class SwapPairInfoConverter : Converter { + private val rateTypeConverter = RateTypeConverter() + override fun convert(value: SwapPair): SwapPairDomain { return SwapPairDomain( from = LeastTokenInfo( @@ -30,14 +30,8 @@ class SwapPairInfoConverter : Converter { private fun convertProvider(swapPairProvider: SwapPairProvider): SwapPairProviderDomain { return SwapPairProviderDomain( providerId = swapPairProvider.providerId, - rateTypes = swapPairProvider.rateTypes.map { convertRateType(it) }, + rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) }, ) } - private fun convertRateType(rateType: RateType): RateTypeDomain { - return when (rateType) { - RateType.FIXED -> RateTypeDomain.FIXED - RateType.FLOAT -> RateTypeDomain.FLOAT - } - } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt index 0b9605485b..cb99b58076 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt @@ -27,7 +27,7 @@ internal class DefaultBlockchainInteractor @Inject constructor( return if (token is CryptoCurrency.Token) { token.decimals } else { - transactionManager.getNativeTokenDecimals(token.network.id.value) + transactionManager.getNativeTokenDecimals(token.network.backendId) } } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 3e63caec40..738424d73c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -107,15 +107,17 @@ internal class SwapInteractorImpl @Inject constructor( tokenInfoForFilter(it).network == currency.network.backendId } - val availableCryptoCurrencies = filteredPairs.mapNotNull { - findCryptoCurrencyStatusByLeastInfo(tokenInfoForAvailable(it), cryptoCurrenciesList) + val availableCryptoCurrencies = filteredPairs.mapNotNull { pair -> + val status = findCryptoCurrencyStatusByLeastInfo(tokenInfoForAvailable(pair), cryptoCurrenciesList) + status?.let { CryptoCurrencySwapInfo(it, pair.providers) } } - val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies.toSet() + val unavailableCryptoCurrencies = + (cryptoCurrenciesList - availableCryptoCurrencies.map { it.currencyStatus }.toSet()) return CurrenciesGroup( available = availableCryptoCurrencies, - unavailable = unavailableCryptoCurrencies, + unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) } ) } @@ -415,7 +417,7 @@ internal class SwapInteractorImpl @Inject constructor( return if (token is CryptoCurrency.Token) { token.decimals } else { - transactionManager.getNativeTokenDecimals(token.network.id.value) + transactionManager.getNativeTokenDecimals(token.network.backendId) } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index ab0e4095c8..8942e75161 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -61,4 +61,14 @@ interface SwapRepository { currency: CryptoCurrency, amount: BigDecimal?, ): String + + suspend fun getExchangeQuote( + fromContractAddress: String, + fromNetwork: String, + toContractAddress: String, + toNetwork: String, + fromAmount: BigDecimal, + providerId: Int, + rateType: RateType, + ) : ExchangeQuote } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt new file mode 100644 index 0000000000..d087b97955 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.swap.domain.models.domain + +import java.math.BigDecimal + +data class ExchangeQuote( + val toAmount: BigDecimal, + val allowanceContract: String? +) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index 704a27dc1b..a41f3d283d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.domain.models.domain -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus /** * Domain layer representation of SwapPair data network model. @@ -12,20 +12,12 @@ import com.tangem.domain.tokens.model.CryptoCurrency data class SwapPairLeast( val from: LeastTokenInfo, val to: LeastTokenInfo, - val providers: List, + val providers: List, ) -/** - * Enriched model of swap pair data. Contains full CryptoCurrency models instead of least info. - * - * @property from CryptoCurrency we want to change - * @property to CryptoCurrency we want to exchange for - * @property providers Exchange providers - */ -data class SwapPair( - val from: CryptoCurrency, - val to: CryptoCurrency, - val providers: List, +data class CryptoCurrencySwapInfo( + val currencyStatus: CryptoCurrencyStatus, + val providers: List ) /** @@ -34,7 +26,7 @@ data class SwapPair( * @property providerId provider id * @property rateTypes supported rate types */ -data class SwapPairProvider( +data class SwapProvider( val providerId: Int, val rateTypes: List, ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index 2401c08574..752e9184a7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo data class TokensDataStateExpress( val fromGroup: CurrenciesGroup, @@ -9,8 +9,8 @@ data class TokensDataStateExpress( ) data class CurrenciesGroup( - val available: List, - val unavailable: List, + val available: List, + val unavailable: List, ) data class FoundTokensStateExpress( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 177f462107..99919c05a3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -132,14 +132,17 @@ internal class SwapViewModel @Inject constructor( runCatching(dispatchers.io) { swapInteractor.getTokensDataState(initialCryptoCurrency) }.onSuccess { state -> - // dataState = dataState.copy( - // ) + dataState = dataState.copy( + fromCryptoCurrency = initialCryptoCurrency, + toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency + ) + // updateTokensState(dataState = state.foundTokensState) - // startLoadingQuotes( - // fromToken = state.preselectTokens.fromToken, - // toToken = state.preselectTokens.toToken, - // amount = lastAmount.value, - // ) + startLoadingQuotes( + fromToken = initialCryptoCurrency, + toToken = state.toGroup.available.first().currencyStatus.currency, + amount = lastAmount.value, + ) }.onFailure { Timber.tag(loggingTag).e(it) } From 616c0b2c05e9b07c4442fcf1263544b97e1351f9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 20 Nov 2023 22:33:11 +0300 Subject: [PATCH 029/139] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 4 +- .../local/quote/DefaultQuotesStore.kt | 21 +++--- ...kt => GetCryptoCurrencyStatusesUseCase.kt} | 22 +----- .../CurrenciesStatusesOperations.kt | 30 ++++++++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 6 +- .../swap/converters/SwapPairInfoConverter.kt | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 25 +++---- .../feature/swap/domain/SwapRepository.kt | 3 +- .../swap/domain/di/SwapDomainModule.kt | 8 +- .../domain/models/domain/ExchangeQuote.kt | 2 +- .../domain/models/domain/SwapPairLeast.kt | 2 +- features/swap/presentation/build.gradle.kts | 3 + .../swap/converters/TokensDataConverter.kt | 74 +++++++++++++------ .../swap/models/SwapSelectTokenStateHolder.kt | 10 +-- .../tangem/feature/swap/ui/StateBuilder.kt | 19 ++--- .../feature/swap/ui/SwapSelectTokenScreen.kt | 13 ++-- .../swap/viewmodels/SwapProcessDataState.kt | 2 + .../feature/swap/viewmodels/SwapViewModel.kt | 51 +++++++++---- 18 files changed, 173 insertions(+), 123 deletions(-) rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{GetCryptoCurrencyStatusUseCase.kt => GetCryptoCurrencyStatusesUseCase.kt} (65%) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 66027e8a6b..80f84ebf33 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -36,8 +36,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json @Suppress("LargeClass") class TradeCryptoMiddleware { @@ -259,7 +257,7 @@ class TradeCryptoMiddleware { private fun openSwap(currency: CryptoCurrency) { val bundle = bundleOf( - SwapFragment.CURRENCY_BUNDLE_KEY to currency + SwapFragment.CURRENCY_BUNDLE_KEY to currency, ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index fc0d1e0e42..e28a90bfb2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -4,8 +4,8 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.datasource.local.quote.model.StoredQuote import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow internal class DefaultQuotesStore( private val dataStore: StringKeyDataStore, @@ -13,20 +13,17 @@ internal class DefaultQuotesStore( override fun get(currenciesIds: Set): Flow> { return channelFlow { - val flows = currenciesIds.mapNotNull { currencyId -> - currencyId.rawCurrencyId?.let(dataStore::get) - } - - if (dataStore.isEmpty() || flows.isEmpty()) { + if (dataStore.isEmpty()) { send(emptySet()) } - merge(*flows.toTypedArray()) - .scan>(emptySet()) { acc, quote -> - acc.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId } + val quotes = currenciesIds.mapNotNull { currencyId -> + currencyId.rawCurrencyId?.let { + dataStore.getSyncOrNull(it) } - .filter(Set::isNotEmpty) - .collect(::send) + } + + send(quotes.toSet()) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt similarity index 65% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt index d917a747c7..2734f1b2e3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt @@ -4,9 +4,7 @@ import arrow.core.Either import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -14,7 +12,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* -class GetCryptoCurrencyStatusUseCase( +class GetCryptoCurrencyStatusesUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, @@ -29,25 +27,9 @@ class GetCryptoCurrencyStatusUseCase( networksRepository = networksRepository, ) - return operations.getCurrenciesStatusesFlow() + return operations.getCurrenciesStatusesMergedFlow() .map { maybeCurrenciesStatuses -> maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) } } - - @Suppress("UnusedPrivateMember") - private fun createTokenList( - userWalletId: UserWalletId, - tokens: List, - ): Flow> { - val operations = TokenListOperations( - userWalletId = userWalletId, - tokens = tokens, - currenciesRepository = currenciesRepository, - ) - - return operations.getTokenListFlow().map { maybeTokenList -> - maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError) - } - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 9bce6f0d35..60e3d3b4b0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -67,6 +67,36 @@ internal class CurrenciesStatusesOperations( } } + fun getCurrenciesStatusesMergedFlow(): Flow>> { + return getMultiCurrencyWalletCurrencies().transformLatest { maybeCurrencies -> + val nonEmptyCurrencies = maybeCurrencies.fold( + ifLeft = { error -> + emit(error.left()) + return@transformLatest + }, + ifRight = List::toNonEmptyListOrNull, + ) + + if (nonEmptyCurrencies == null) { + val emptyCurrenciesStatuses = emptyList() + + emit(emptyCurrenciesStatuses.right()) + return@transformLatest + } + + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + + val currenciesFlow = combine( + getQuotes(currenciesIds), + getNetworksStatuses(networks), + ) { maybeQuotes, maybeNetworksStatuses -> + createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + } + + emitAll(currenciesFlow) + } + } + fun getCardCurrenciesStatusesFlow(): Flow>> { return flow { val nonEmptyCurrencies = recover( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 9eb5bedf15..9aa4fc368b 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -265,7 +265,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork: String, fromAmount: BigDecimal, providerId: Int, - rateType: RateType + rateType: RateType, ): ExchangeQuote { val response = tangemExpressApi.getExchangeQuote( fromContractAddress, @@ -274,12 +274,12 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork, fromAmount, providerId, - rateTypeConverter.convertBack(rateType) + rateTypeConverter.convertBack(rateType), ).getOrThrow() return ExchangeQuote( toAmount = response.toAmount, - allowanceContract = response.allowanceContract + allowanceContract = response.allowanceContract, ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index e1e48fc630..c205bec15e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -33,5 +33,4 @@ class SwapPairInfoConverter : Converter { rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) }, ) } - } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 738424d73c..f9d1faa5bb 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network @@ -39,7 +39,7 @@ internal class SwapInteractorImpl @Inject constructor( private val networksRepository: NetworksRepository, private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusUseCase, + private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesUseCase, ) : SwapInteractor { // TODO: Move to DI @@ -58,7 +58,7 @@ internal class SwapInteractorImpl @Inject constructor( ifRight = { it }, ) - requireNotNull(selectedWallet) + requireNotNull(selectedWallet) { "No selected wallet" } val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) .first() @@ -112,12 +112,13 @@ internal class SwapInteractorImpl @Inject constructor( status?.let { CryptoCurrencySwapInfo(it, pair.providers) } } - val unavailableCryptoCurrencies = - (cryptoCurrenciesList - availableCryptoCurrencies.map { it.currencyStatus }.toSet()) + val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies + .map { it.currencyStatus } + .toSet() return CurrenciesGroup( available = availableCryptoCurrencies, - unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) } + unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) }, ) } @@ -138,14 +139,10 @@ internal class SwapInteractorImpl @Inject constructor( } } - private fun Currency.getContractAddress(): String { - return when (this) { - is Currency.NativeToken -> "0" - is Currency.NonNativeToken -> this.contractAddress - } - } - - suspend fun getPairs(initialCurrency: LeastTokenInfo, currenciesList: List): List { + private suspend fun getPairs( + initialCurrency: LeastTokenInfo, + currenciesList: List, + ): List { return repository.getPairs(initialCurrency, currenciesList) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 8942e75161..d64b0fa8f0 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -62,6 +62,7 @@ interface SwapRepository { amount: BigDecimal?, ): String + @Suppress("LongParameterList") suspend fun getExchangeQuote( fromContractAddress: String, fromNetwork: String, @@ -70,5 +71,5 @@ interface SwapRepository { fromAmount: BigDecimal, providerId: Int, rateType: RateType, - ) : ExchangeQuote + ): ExchangeQuote } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index dc882859ae..3169f16427 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain.di import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -35,7 +35,7 @@ class SwapDomainModule { networksRepository: NetworksRepository, walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusUseCase, + @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesUseCase, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -81,8 +81,8 @@ class SwapDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, - ): GetCryptoCurrencyStatusUseCase { - return GetCryptoCurrencyStatusUseCase( + ): GetCryptoCurrencyStatusesUseCase { + return GetCryptoCurrencyStatusesUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt index d087b97955..ee352e70c4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt @@ -4,5 +4,5 @@ import java.math.BigDecimal data class ExchangeQuote( val toAmount: BigDecimal, - val allowanceContract: String? + val allowanceContract: String?, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index a41f3d283d..ee772becce 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -17,7 +17,7 @@ data class SwapPairLeast( data class CryptoCurrencySwapInfo( val currencyStatus: CryptoCurrencyStatus, - val providers: List + val providers: List, ) /** diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 1068c49d28..8be06eaf61 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -17,6 +17,8 @@ dependencies { implementation(projects.common) /** Domain modules **/ + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) @@ -28,6 +30,7 @@ dependencies { implementation(deps.androidx.browser) /** Compose */ + implementation(deps.arrow.core) implementation(deps.compose.foundation) implementation(deps.compose.material) implementation(deps.compose.ui.tooling) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index bb52e2b01d..03047e24b8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -2,52 +2,80 @@ package com.tangem.feature.swap.converters import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.TokenIconState -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.feature.swap.domain.models.domain.NetworkInfo -import com.tangem.feature.swap.domain.models.ui.FoundTokensStateExpress -import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress -import com.tangem.feature.swap.models.Network +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelectState +import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList class TokensDataConverter( private val onSearchEntered: (String) -> Unit, private val onTokenSelected: (String) -> Unit, private val isBalanceHiddenProvider: Provider, -) { + private val appCurrencyProvider: Provider, +) : Converter { - fun convertWithNetwork(value: FoundTokensStateExpress, network: NetworkInfo): SwapSelectTokenStateHolder { + override fun convert(value: CurrenciesGroup): SwapSelectTokenStateHolder { + val availableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource + val unavailableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource return SwapSelectTokenStateHolder( - availableTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), - unavailableTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), + availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it) } + .toMutableList() + .apply { + this.add(0, availableTitle) + } + .toImmutableList(), + unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it) } + .toMutableList() + .apply { + this.add(0, unavailableTitle) + } + .toImmutableList(), onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, - network = Network(network.name, network.blockchainId), ) } - private fun tokenWithBalanceToTokenToSelect( - tokenWithBalance: TokenWithBalanceExpress, - ): TokenToSelectState.TokenToSelect { + private fun tokenWithBalanceToTokenToSelect(cryptoCurrencySwapInfo: CryptoCurrencySwapInfo): TokenToSelectState { + val cryptoCurrencyStatus = cryptoCurrencySwapInfo.currencyStatus return TokenToSelectState.TokenToSelect( - id = tokenWithBalance.token.id.value, - name = tokenWithBalance.token.name, - symbol = tokenWithBalance.token.symbol, - isNative = tokenWithBalance.token is CryptoCurrency.Coin, - // todo replace converting + id = cryptoCurrencyStatus.currency.id.value, + name = cryptoCurrencyStatus.currency.name, + symbol = cryptoCurrencyStatus.currency.symbol, tokenIcon = TokenIconState.CoinIcon( - url = "", - fallbackResId = 0, + url = cryptoCurrencyStatus.currency.iconUrl, + fallbackResId = cryptoCurrencyStatus.currency.networkIconResId, isGrayscale = false, - showCustomBadge = false, + showCustomBadge = cryptoCurrencyStatus.currency.isCustom, ), addedTokenBalanceData = TokenBalanceData( - amount = tokenWithBalance.tokenBalanceData?.amount, - amountEquivalent = tokenWithBalance.tokenBalanceData?.amountEquivalent, + amount = formatCryptoAmount(cryptoCurrencyStatus), + amountEquivalent = formatFiatAmount(cryptoCurrencyStatus, appCurrencyProvider.invoke()), isBalanceHidden = isBalanceHiddenProvider.invoke(), ), ) } + + private fun formatCryptoAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): String { + return BigDecimalFormatter.formatCryptoAmount( + cryptoCurrencyStatus.value.amount, + cryptoCurrencyStatus.currency.symbol, + cryptoCurrencyStatus.currency.decimals, + ) + } + + private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = cryptoCurrencyStatus.value.fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 382da8e588..ac6762d7b7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -1,30 +1,24 @@ package com.tangem.feature.swap.models import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList data class SwapSelectTokenStateHolder( val availableTokens: ImmutableList, val unavailableTokens: ImmutableList, - val network: Network, val onSearchEntered: (String) -> Unit, val onTokenSelected: (String) -> Unit, ) -data class Network( - val name: String, - val blockchainId: String, -) - sealed class TokenToSelectState { - data class Title(val title: String) : TokenToSelectState() + data class Title(val title: TextReference) : TokenToSelectState() data class TokenToSelect( val id: String, val name: String, val symbol: String, - val isNative: Boolean, val tokenIcon: TokenIconState, val available: Boolean = true, val addedTokenBalanceData: TokenBalanceData? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index bea53175af..510502e088 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError @@ -24,12 +25,17 @@ import kotlinx.collections.immutable.toImmutableList * State builder creates a specific states for SwapScreen */ @Suppress("LargeClass") -internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: Provider) { +internal class StateBuilder( + private val actions: UiActions, + private val isBalanceHiddenProvider: Provider, + appCurrencyProvider: Provider, +) { private val tokensDataConverter = TokensDataConverter( onSearchEntered = actions.onSearchEntered, onTokenSelected = actions.onTokenSelected, isBalanceHiddenProvider = isBalanceHiddenProvider, + appCurrencyProvider = appCurrencyProvider, ) fun createInitialLoadingState(initialCurrency: CryptoCurrency, networkInfo: NetworkInfo): SwapStateHolder { @@ -247,15 +253,10 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: ) } - fun addTokensToState( - uiState: SwapStateHolder, - dataState: FoundTokensStateExpress, - networkInfo: NetworkInfo, - ): SwapStateHolder { + fun addTokensToState(uiState: SwapStateHolder, tokensDataState: CurrenciesGroup): SwapStateHolder { return uiState.copy( - selectTokenState = tokensDataConverter.convertWithNetwork( - value = dataState, - network = networkInfo, + selectTokenState = tokensDataConverter.convert( + value = tokensDataState, ), ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 1faa262bb3..2bbeedc808 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.Strings @@ -20,7 +19,8 @@ import com.tangem.core.ui.components.appbar.ExpandableSearchView import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.R @@ -43,8 +43,7 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) placeholderSearchText = stringResource(id = R.string.common_search_tokens), onSearchChange = state.onSearchEntered, onSearchDisplayClose = { state.onSearchEntered("") }, - subtitle = state.network.name, - icon = painterResource(id = getActiveIconRes(state.network.blockchainId)), + subtitle = "", // todo add title ) }, ) @@ -112,7 +111,7 @@ private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Mod .background(TangemTheme.colors.background.action), ) { Text( - text = item.title, + text = item.title.resolveReference(), style = TangemTheme.typography.overline, modifier = Modifier .padding( @@ -220,7 +219,6 @@ private val token = TokenToSelectState.TokenToSelect( id = "", name = "USDC", symbol = "USDC", - isNative = false, addedTokenBalanceData = TokenBalanceData( amount = "15 000 $", amountEquivalent = "15 000 " + @@ -230,7 +228,7 @@ private val token = TokenToSelectState.TokenToSelect( ) private val title = TokenToSelectState.Title( - title = "MY TOKENS", + title = stringReference("MY TOKENS"), ) @Preview @@ -243,7 +241,6 @@ private fun TokenScreenPreview() { unavailableTokens = listOf(title, token, token, token).toImmutableList(), onSearchEntered = {}, onTokenSelected = {}, - network = Network("Ethereum", "ETH"), ), onBack = {}, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index 65abcf814c..c9018a2345 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData import com.tangem.feature.swap.domain.models.ui.SwapStateData +import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress import com.tangem.feature.swap.domain.models.ui.TxFee data class SwapProcessDataState( @@ -20,4 +21,5 @@ data class SwapProcessDataState( val approveDataModel: RequestApproveStateData? = null, val swapDataModel: SwapStateData? = null, val selectedFee: TxFee? = null, + val tokensDataState: TokensDataStateExpress? = null, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 99919c05a3..0f2d531f35 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -4,9 +4,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* +import arrow.core.getOrElse import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.analytics.SwapEvents @@ -27,11 +30,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.runCatching import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json import timber.log.Timber import java.text.DecimalFormat import java.text.NumberFormat @@ -47,6 +48,7 @@ internal class SwapViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -55,9 +57,12 @@ internal class SwapViewModel @Inject constructor( private var isBalanceHidden = true + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private val stateBuilder = StateBuilder( actions = createUiActions(), isBalanceHiddenProvider = Provider { isBalanceHidden }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), ) private val inputNumberFormatter = @@ -87,7 +92,7 @@ internal class SwapViewModel @Inject constructor( derivationPath = initialCryptoCurrency.network.derivationPath.value, network = initialCryptoCurrency.network, ) - initTokens(initialCryptoCurrency) + initTokens() } override fun onCreate(owner: LifecycleOwner) { @@ -126,7 +131,7 @@ internal class SwapViewModel @Inject constructor( } @Suppress("UnusedPrivateMember") - private fun initTokens(currency: CryptoCurrency) { + private fun initTokens() { // new flow viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { @@ -134,10 +139,11 @@ internal class SwapViewModel @Inject constructor( }.onSuccess { state -> dataState = dataState.copy( fromCryptoCurrency = initialCryptoCurrency, - toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency + toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency, + tokensDataState = state, ) - // updateTokensState(dataState = state.foundTokensState) + updateTokensState(state) startLoadingQuotes( fromToken = initialCryptoCurrency, toToken = state.toGroup.available.first().currencyStatus.currency, @@ -171,11 +177,11 @@ internal class SwapViewModel @Inject constructor( // } } - private fun updateTokensState(dataState: FoundTokensStateExpress) { + private fun updateTokensState(dataState: TokensDataStateExpress) { + val tokensDataState = if (!isOrderReversed) dataState.toGroup else dataState.fromGroup uiState = stateBuilder.addTokensToState( uiState = uiState, - dataState = dataState, - networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId), + tokensDataState = tokensDataState, ) } @@ -369,28 +375,31 @@ internal class SwapViewModel @Inject constructor( swapInteractor.searchTokens(dataState.networkId, searchQuery) } .onSuccess { - updateTokensState(it) + // updateTokensState(it) } .onFailure { } } } private fun onTokenSelect(id: String) { - val foundToken = swapInteractor.findTokenById(id) + val tokens = dataState.tokensDataState ?: return + val foundToken = tokens.toGroup.available.firstOrNull { + it.currencyStatus.currency.id.value == id + } analyticsEventHandler.send( - event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.symbol), + event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.currencyStatus?.currency?.symbol), ) if (foundToken != null) { val fromToken: CryptoCurrency val toToken: CryptoCurrency if (isOrderReversed) { - fromToken = foundToken + fromToken = foundToken.currencyStatus.currency toToken = initialCryptoCurrency } else { fromToken = initialCryptoCurrency - toToken = foundToken + toToken = foundToken.currencyStatus.currency } dataState = dataState.copy( fromCryptoCurrency = fromToken, @@ -532,6 +541,18 @@ internal class SwapViewModel @Inject constructor( ) } + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" From 60d8a4f2a320313fd773e906f18093c96ab9c97d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 12:03:02 +0300 Subject: [PATCH 030/139] Updated on 2026-08-14 --- .../local/quote/DefaultQuotesStore.kt | 21 +++++---- ...> GetCryptoCurrencyStatusesSyncUseCase.kt} | 11 ++--- .../CurrenciesStatusesOperations.kt | 38 ++++++--------- .../feature/swap/domain/SwapInteractorImpl.kt | 6 +-- .../swap/domain/di/SwapDomainModule.kt | 8 ++-- .../swap/converters/TokensDataConverter.kt | 46 +++++++++++++++---- .../feature/swap/ui/SwapSelectTokenScreen.kt | 21 +++++---- 7 files changed, 83 insertions(+), 68 deletions(-) rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{GetCryptoCurrencyStatusesUseCase.kt => GetCryptoCurrencyStatusesSyncUseCase.kt} (73%) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index e28a90bfb2..fc0d1e0e42 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -4,8 +4,8 @@ import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.datasource.local.quote.model.StoredQuote import com.tangem.domain.tokens.model.CryptoCurrency -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.* internal class DefaultQuotesStore( private val dataStore: StringKeyDataStore, @@ -13,17 +13,20 @@ internal class DefaultQuotesStore( override fun get(currenciesIds: Set): Flow> { return channelFlow { - if (dataStore.isEmpty()) { + val flows = currenciesIds.mapNotNull { currencyId -> + currencyId.rawCurrencyId?.let(dataStore::get) + } + + if (dataStore.isEmpty() || flows.isEmpty()) { send(emptySet()) } - val quotes = currenciesIds.mapNotNull { currencyId -> - currencyId.rawCurrencyId?.let { - dataStore.getSyncOrNull(it) + merge(*flows.toTypedArray()) + .scan>(emptySet()) { acc, quote -> + acc.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId } } - } - - send(quotes.toSet()) + .filter(Set::isNotEmpty) + .collect(::send) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt similarity index 73% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt index 2734f1b2e3..cdf58b3080 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt @@ -10,16 +10,15 @@ import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* -class GetCryptoCurrencyStatusesUseCase( +class GetCryptoCurrencyStatusesSyncUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, internal val dispatchers: CoroutineDispatcherProvider, ) { - operator fun invoke(userWalletId: UserWalletId): Flow>> { + suspend operator fun invoke(userWalletId: UserWalletId): Either> { val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, currenciesRepository = currenciesRepository, @@ -27,9 +26,7 @@ class GetCryptoCurrencyStatusesUseCase( networksRepository = networksRepository, ) - return operations.getCurrenciesStatusesMergedFlow() - .map { maybeCurrenciesStatuses -> - maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) - } + return operations.getCurrenciesStatusesSync() + .mapLeft { error -> error.mapToTokenListError() } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 60e3d3b4b0..3ce6112296 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -67,33 +67,21 @@ internal class CurrenciesStatusesOperations( } } - fun getCurrenciesStatusesMergedFlow(): Flow>> { - return getMultiCurrencyWalletCurrencies().transformLatest { maybeCurrencies -> - val nonEmptyCurrencies = maybeCurrencies.fold( - ifLeft = { error -> - emit(error.left()) - return@transformLatest + suspend fun getCurrenciesStatusesSync(): Either> { + return either { + catch( + block = { + val nonEmptyCurrencies = + currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull() + ?: return emptyList().right() + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right() + val networkStatuses = + networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() + return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses) }, - ifRight = List::toNonEmptyListOrNull, + catch = { raise(Error.DataError(it)) }, ) - - if (nonEmptyCurrencies == null) { - val emptyCurrenciesStatuses = emptyList() - - emit(emptyCurrenciesStatuses.right()) - return@transformLatest - } - - val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - - val currenciesFlow = combine( - getQuotes(currenciesIds), - getNetworksStatuses(networks), - ) { maybeQuotes, maybeNetworksStatuses -> - createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) - } - - emitAll(currenciesFlow) } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index f9d1faa5bb..ec089f6df7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusesUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network @@ -22,7 +22,6 @@ import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.toFiatString -import kotlinx.coroutines.flow.first import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -39,7 +38,7 @@ internal class SwapInteractorImpl @Inject constructor( private val networksRepository: NetworksRepository, private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesUseCase, + private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, ) : SwapInteractor { // TODO: Move to DI @@ -61,7 +60,6 @@ internal class SwapInteractorImpl @Inject constructor( requireNotNull(selectedWallet) { "No selected wallet" } val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) - .first() .getOrElse { emptyList() } val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses.filter { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 3169f16427..7fa93f1c70 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain.di import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusesUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -35,7 +35,7 @@ class SwapDomainModule { networksRepository: NetworksRepository, walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesUseCase, + @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -81,8 +81,8 @@ class SwapDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, - ): GetCryptoCurrencyStatusesUseCase { - return GetCryptoCurrencyStatusesUseCase( + ): GetCryptoCurrencyStatusesSyncUseCase { + return GetCryptoCurrencyStatusesSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 03047e24b8..e3eff35d4c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -2,10 +2,13 @@ package com.tangem.feature.swap.converters import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.getTintForTokenIcon import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup @@ -26,13 +29,13 @@ class TokensDataConverter( val availableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource val unavailableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource return SwapSelectTokenStateHolder( - availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it) } + availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) } .toMutableList() .apply { this.add(0, availableTitle) } .toImmutableList(), - unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it) } + unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) } .toMutableList() .apply { this.add(0, unavailableTitle) @@ -43,18 +46,17 @@ class TokensDataConverter( ) } - private fun tokenWithBalanceToTokenToSelect(cryptoCurrencySwapInfo: CryptoCurrencySwapInfo): TokenToSelectState { + private fun tokenWithBalanceToTokenToSelect( + cryptoCurrencySwapInfo: CryptoCurrencySwapInfo, + isAvailable: Boolean, + ): TokenToSelectState { val cryptoCurrencyStatus = cryptoCurrencySwapInfo.currencyStatus return TokenToSelectState.TokenToSelect( id = cryptoCurrencyStatus.currency.id.value, name = cryptoCurrencyStatus.currency.name, symbol = cryptoCurrencyStatus.currency.symbol, - tokenIcon = TokenIconState.CoinIcon( - url = cryptoCurrencyStatus.currency.iconUrl, - fallbackResId = cryptoCurrencyStatus.currency.networkIconResId, - isGrayscale = false, - showCustomBadge = cryptoCurrencyStatus.currency.isCustom, - ), + available = isAvailable, + tokenIcon = convertIcon(cryptoCurrencyStatus.currency, isAvailable), addedTokenBalanceData = TokenBalanceData( amount = formatCryptoAmount(cryptoCurrencyStatus), amountEquivalent = formatFiatAmount(cryptoCurrencyStatus, appCurrencyProvider.invoke()), @@ -63,6 +65,32 @@ class TokensDataConverter( ) } + private fun convertIcon(currency: CryptoCurrency, isAvailable: Boolean): TokenIconState { + return when (currency) { + is CryptoCurrency.Coin -> { + TokenIconState.CoinIcon( + url = currency.iconUrl, + fallbackResId = currency.networkIconResId, + isGrayscale = !isAvailable, + showCustomBadge = currency.isCustom, + ) + } + is CryptoCurrency.Token -> { + val isGrayscale = currency.network.isTestnet + val background = currency.tryGetBackgroundForTokenIcon(isGrayscale) + val tint = getTintForTokenIcon(background) + TokenIconState.TokenIcon( + url = currency.iconUrl, + isGrayscale = !isAvailable, + showCustomBadge = currency.isCustom, + networkBadgeIconResId = currency.networkIconResId, + fallbackTint = tint, + fallbackBackground = background, + ) + } + } + } + private fun formatCryptoAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): String { return BigDecimalFormatter.formatCryptoAmount( cryptoCurrencyStatus.value.amount, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 2bbeedc808..6e89f78c3a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -111,7 +111,7 @@ private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Mod .background(TangemTheme.colors.background.action), ) { Text( - text = item.title.resolveReference(), + text = item.title.resolveReference().uppercase(), style = TangemTheme.typography.overline, modifier = Modifier .padding( @@ -133,7 +133,10 @@ private fun TokenItem( modifier = modifier .fillMaxWidth() .height(TangemTheme.dimens.size72) - .clickable(onClick = onTokenClick) + .clickable( + enabled = token.available, + onClick = onTokenClick, + ) .padding( vertical = TangemTheme.dimens.spacing14, horizontal = TangemTheme.dimens.spacing16, @@ -169,13 +172,7 @@ private fun TokenItem( Spacer(modifier = Modifier.weight(1f)) - if (!token.available) { - Text( - text = stringResource(id = R.string.swapping_token_not_available), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } else if (token.addedTokenBalanceData != null) { + if (token.addedTokenBalanceData != null) { Column( horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Center, @@ -190,7 +187,11 @@ private fun TokenItem( token.addedTokenBalanceData.amountEquivalent.orEmpty() }, style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, + color = if (token.available) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.tertiary + }, ) SpacerW2() Text( From 3c5c53ce92e41ddff8945b27f05411967eb404ad Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 12:42:58 +0300 Subject: [PATCH 031/139] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 4 +- ...> GetCryptoCurrencyStatusesSyncUseCase.kt} | 29 +---- .../CurrenciesStatusesOperations.kt | 18 +++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 6 +- .../swap/converters/SwapPairInfoConverter.kt | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 27 ++--- .../feature/swap/domain/SwapRepository.kt | 3 +- .../swap/domain/di/SwapDomainModule.kt | 8 +- .../domain/models/domain/ExchangeQuote.kt | 2 +- .../domain/models/domain/SwapPairLeast.kt | 2 +- features/swap/presentation/build.gradle.kts | 3 + .../swap/converters/TokensDataConverter.kt | 104 ++++++++++++++---- .../swap/models/SwapSelectTokenStateHolder.kt | 10 +- .../tangem/feature/swap/ui/StateBuilder.kt | 19 ++-- .../feature/swap/ui/SwapSelectTokenScreen.kt | 32 +++--- .../swap/viewmodels/SwapProcessDataState.kt | 2 + .../feature/swap/viewmodels/SwapViewModel.kt | 51 ++++++--- 17 files changed, 193 insertions(+), 128 deletions(-) rename domain/tokens/src/main/kotlin/com/tangem/domain/tokens/{GetCryptoCurrencyStatusUseCase.kt => GetCryptoCurrencyStatusesSyncUseCase.kt} (51%) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 66027e8a6b..80f84ebf33 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -36,8 +36,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.launch -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json @Suppress("LargeClass") class TradeCryptoMiddleware { @@ -259,7 +257,7 @@ class TradeCryptoMiddleware { private fun openSwap(currency: CryptoCurrency) { val bundle = bundleOf( - SwapFragment.CURRENCY_BUNDLE_KEY to currency + SwapFragment.CURRENCY_BUNDLE_KEY to currency, ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt similarity index 51% rename from domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt rename to domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt index d917a747c7..cdf58b3080 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt @@ -4,24 +4,21 @@ import arrow.core.Either import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* -class GetCryptoCurrencyStatusUseCase( +class GetCryptoCurrencyStatusesSyncUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, internal val dispatchers: CoroutineDispatcherProvider, ) { - operator fun invoke(userWalletId: UserWalletId): Flow>> { + suspend operator fun invoke(userWalletId: UserWalletId): Either> { val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, currenciesRepository = currenciesRepository, @@ -29,25 +26,7 @@ class GetCryptoCurrencyStatusUseCase( networksRepository = networksRepository, ) - return operations.getCurrenciesStatusesFlow() - .map { maybeCurrenciesStatuses -> - maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) - } - } - - @Suppress("UnusedPrivateMember") - private fun createTokenList( - userWalletId: UserWalletId, - tokens: List, - ): Flow> { - val operations = TokenListOperations( - userWalletId = userWalletId, - tokens = tokens, - currenciesRepository = currenciesRepository, - ) - - return operations.getTokenListFlow().map { maybeTokenList -> - maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError) - } + return operations.getCurrenciesStatusesSync() + .mapLeft { error -> error.mapToTokenListError() } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 9bce6f0d35..3ce6112296 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -67,6 +67,24 @@ internal class CurrenciesStatusesOperations( } } + suspend fun getCurrenciesStatusesSync(): Either> { + return either { + catch( + block = { + val nonEmptyCurrencies = + currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId).toNonEmptyListOrNull() + ?: return emptyList().right() + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right() + val networkStatuses = + networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() + return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses) + }, + catch = { raise(Error.DataError(it)) }, + ) + } + } + fun getCardCurrenciesStatusesFlow(): Flow>> { return flow { val nonEmptyCurrencies = recover( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 9eb5bedf15..9aa4fc368b 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -265,7 +265,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork: String, fromAmount: BigDecimal, providerId: Int, - rateType: RateType + rateType: RateType, ): ExchangeQuote { val response = tangemExpressApi.getExchangeQuote( fromContractAddress, @@ -274,12 +274,12 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork, fromAmount, providerId, - rateTypeConverter.convertBack(rateType) + rateTypeConverter.convertBack(rateType), ).getOrThrow() return ExchangeQuote( toAmount = response.toAmount, - allowanceContract = response.allowanceContract + allowanceContract = response.allowanceContract, ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index e1e48fc630..c205bec15e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -33,5 +33,4 @@ class SwapPairInfoConverter : Converter { rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) }, ) } - } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 738424d73c..ec089f6df7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network @@ -22,7 +22,6 @@ import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.toFiatString -import kotlinx.coroutines.flow.first import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -39,7 +38,7 @@ internal class SwapInteractorImpl @Inject constructor( private val networksRepository: NetworksRepository, private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusUseCase, + private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, ) : SwapInteractor { // TODO: Move to DI @@ -58,10 +57,9 @@ internal class SwapInteractorImpl @Inject constructor( ifRight = { it }, ) - requireNotNull(selectedWallet) + requireNotNull(selectedWallet) { "No selected wallet" } val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) - .first() .getOrElse { emptyList() } val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses.filter { @@ -112,12 +110,13 @@ internal class SwapInteractorImpl @Inject constructor( status?.let { CryptoCurrencySwapInfo(it, pair.providers) } } - val unavailableCryptoCurrencies = - (cryptoCurrenciesList - availableCryptoCurrencies.map { it.currencyStatus }.toSet()) + val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies + .map { it.currencyStatus } + .toSet() return CurrenciesGroup( available = availableCryptoCurrencies, - unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) } + unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) }, ) } @@ -138,14 +137,10 @@ internal class SwapInteractorImpl @Inject constructor( } } - private fun Currency.getContractAddress(): String { - return when (this) { - is Currency.NativeToken -> "0" - is Currency.NonNativeToken -> this.contractAddress - } - } - - suspend fun getPairs(initialCurrency: LeastTokenInfo, currenciesList: List): List { + private suspend fun getPairs( + initialCurrency: LeastTokenInfo, + currenciesList: List, + ): List { return repository.getPairs(initialCurrency, currenciesList) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 8942e75161..d64b0fa8f0 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -62,6 +62,7 @@ interface SwapRepository { amount: BigDecimal?, ): String + @Suppress("LongParameterList") suspend fun getExchangeQuote( fromContractAddress: String, fromNetwork: String, @@ -70,5 +71,5 @@ interface SwapRepository { fromAmount: BigDecimal, providerId: Int, rateType: RateType, - ) : ExchangeQuote + ): ExchangeQuote } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index dc882859ae..7fa93f1c70 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain.di import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -35,7 +35,7 @@ class SwapDomainModule { networksRepository: NetworksRepository, walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusUseCase, + @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -81,8 +81,8 @@ class SwapDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, dispatchers: CoroutineDispatcherProvider, - ): GetCryptoCurrencyStatusUseCase { - return GetCryptoCurrencyStatusUseCase( + ): GetCryptoCurrencyStatusesSyncUseCase { + return GetCryptoCurrencyStatusesSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt index d087b97955..ee352e70c4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt @@ -4,5 +4,5 @@ import java.math.BigDecimal data class ExchangeQuote( val toAmount: BigDecimal, - val allowanceContract: String? + val allowanceContract: String?, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index a41f3d283d..ee772becce 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -17,7 +17,7 @@ data class SwapPairLeast( data class CryptoCurrencySwapInfo( val currencyStatus: CryptoCurrencyStatus, - val providers: List + val providers: List, ) /** diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 1068c49d28..8be06eaf61 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -17,6 +17,8 @@ dependencies { implementation(projects.common) /** Domain modules **/ + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) @@ -28,6 +30,7 @@ dependencies { implementation(deps.androidx.browser) /** Compose */ + implementation(deps.arrow.core) implementation(deps.compose.foundation) implementation(deps.compose.material) implementation(deps.compose.ui.tooling) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index bb52e2b01d..e3eff35d4c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -2,52 +2,108 @@ package com.tangem.feature.swap.converters import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.getTintForTokenIcon +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.feature.swap.domain.models.domain.NetworkInfo -import com.tangem.feature.swap.domain.models.ui.FoundTokensStateExpress -import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress -import com.tangem.feature.swap.models.Network +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelectState +import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList class TokensDataConverter( private val onSearchEntered: (String) -> Unit, private val onTokenSelected: (String) -> Unit, private val isBalanceHiddenProvider: Provider, -) { + private val appCurrencyProvider: Provider, +) : Converter { - fun convertWithNetwork(value: FoundTokensStateExpress, network: NetworkInfo): SwapSelectTokenStateHolder { + override fun convert(value: CurrenciesGroup): SwapSelectTokenStateHolder { + val availableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource + val unavailableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource return SwapSelectTokenStateHolder( - availableTokens = value.tokensInWallet.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), - unavailableTokens = value.loadedTokens.map { tokenWithBalanceToTokenToSelect(it) }.toImmutableList(), + availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) } + .toMutableList() + .apply { + this.add(0, availableTitle) + } + .toImmutableList(), + unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) } + .toMutableList() + .apply { + this.add(0, unavailableTitle) + } + .toImmutableList(), onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, - network = Network(network.name, network.blockchainId), ) } private fun tokenWithBalanceToTokenToSelect( - tokenWithBalance: TokenWithBalanceExpress, - ): TokenToSelectState.TokenToSelect { + cryptoCurrencySwapInfo: CryptoCurrencySwapInfo, + isAvailable: Boolean, + ): TokenToSelectState { + val cryptoCurrencyStatus = cryptoCurrencySwapInfo.currencyStatus return TokenToSelectState.TokenToSelect( - id = tokenWithBalance.token.id.value, - name = tokenWithBalance.token.name, - symbol = tokenWithBalance.token.symbol, - isNative = tokenWithBalance.token is CryptoCurrency.Coin, - // todo replace converting - tokenIcon = TokenIconState.CoinIcon( - url = "", - fallbackResId = 0, - isGrayscale = false, - showCustomBadge = false, - ), + id = cryptoCurrencyStatus.currency.id.value, + name = cryptoCurrencyStatus.currency.name, + symbol = cryptoCurrencyStatus.currency.symbol, + available = isAvailable, + tokenIcon = convertIcon(cryptoCurrencyStatus.currency, isAvailable), addedTokenBalanceData = TokenBalanceData( - amount = tokenWithBalance.tokenBalanceData?.amount, - amountEquivalent = tokenWithBalance.tokenBalanceData?.amountEquivalent, + amount = formatCryptoAmount(cryptoCurrencyStatus), + amountEquivalent = formatFiatAmount(cryptoCurrencyStatus, appCurrencyProvider.invoke()), isBalanceHidden = isBalanceHiddenProvider.invoke(), ), ) } + + private fun convertIcon(currency: CryptoCurrency, isAvailable: Boolean): TokenIconState { + return when (currency) { + is CryptoCurrency.Coin -> { + TokenIconState.CoinIcon( + url = currency.iconUrl, + fallbackResId = currency.networkIconResId, + isGrayscale = !isAvailable, + showCustomBadge = currency.isCustom, + ) + } + is CryptoCurrency.Token -> { + val isGrayscale = currency.network.isTestnet + val background = currency.tryGetBackgroundForTokenIcon(isGrayscale) + val tint = getTintForTokenIcon(background) + TokenIconState.TokenIcon( + url = currency.iconUrl, + isGrayscale = !isAvailable, + showCustomBadge = currency.isCustom, + networkBadgeIconResId = currency.networkIconResId, + fallbackTint = tint, + fallbackBackground = background, + ) + } + } + } + + private fun formatCryptoAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): String { + return BigDecimalFormatter.formatCryptoAmount( + cryptoCurrencyStatus.value.amount, + cryptoCurrencyStatus.currency.symbol, + cryptoCurrencyStatus.currency.decimals, + ) + } + + private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = cryptoCurrencyStatus.value.fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 382da8e588..ac6762d7b7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -1,30 +1,24 @@ package com.tangem.feature.swap.models import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList data class SwapSelectTokenStateHolder( val availableTokens: ImmutableList, val unavailableTokens: ImmutableList, - val network: Network, val onSearchEntered: (String) -> Unit, val onTokenSelected: (String) -> Unit, ) -data class Network( - val name: String, - val blockchainId: String, -) - sealed class TokenToSelectState { - data class Title(val title: String) : TokenToSelectState() + data class Title(val title: TextReference) : TokenToSelectState() data class TokenToSelect( val id: String, val name: String, val symbol: String, - val isNative: Boolean, val tokenIcon: TokenIconState, val available: Boolean = true, val addedTokenBalanceData: TokenBalanceData? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index bea53175af..510502e088 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError @@ -24,12 +25,17 @@ import kotlinx.collections.immutable.toImmutableList * State builder creates a specific states for SwapScreen */ @Suppress("LargeClass") -internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: Provider) { +internal class StateBuilder( + private val actions: UiActions, + private val isBalanceHiddenProvider: Provider, + appCurrencyProvider: Provider, +) { private val tokensDataConverter = TokensDataConverter( onSearchEntered = actions.onSearchEntered, onTokenSelected = actions.onTokenSelected, isBalanceHiddenProvider = isBalanceHiddenProvider, + appCurrencyProvider = appCurrencyProvider, ) fun createInitialLoadingState(initialCurrency: CryptoCurrency, networkInfo: NetworkInfo): SwapStateHolder { @@ -247,15 +253,10 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: ) } - fun addTokensToState( - uiState: SwapStateHolder, - dataState: FoundTokensStateExpress, - networkInfo: NetworkInfo, - ): SwapStateHolder { + fun addTokensToState(uiState: SwapStateHolder, tokensDataState: CurrenciesGroup): SwapStateHolder { return uiState.copy( - selectTokenState = tokensDataConverter.convertWithNetwork( - value = dataState, - network = networkInfo, + selectTokenState = tokensDataConverter.convert( + value = tokensDataState, ), ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 1faa262bb3..6e89f78c3a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.Strings @@ -20,7 +19,8 @@ import com.tangem.core.ui.components.appbar.ExpandableSearchView import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.R @@ -43,8 +43,7 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) placeholderSearchText = stringResource(id = R.string.common_search_tokens), onSearchChange = state.onSearchEntered, onSearchDisplayClose = { state.onSearchEntered("") }, - subtitle = state.network.name, - icon = painterResource(id = getActiveIconRes(state.network.blockchainId)), + subtitle = "", // todo add title ) }, ) @@ -112,7 +111,7 @@ private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Mod .background(TangemTheme.colors.background.action), ) { Text( - text = item.title, + text = item.title.resolveReference().uppercase(), style = TangemTheme.typography.overline, modifier = Modifier .padding( @@ -134,7 +133,10 @@ private fun TokenItem( modifier = modifier .fillMaxWidth() .height(TangemTheme.dimens.size72) - .clickable(onClick = onTokenClick) + .clickable( + enabled = token.available, + onClick = onTokenClick, + ) .padding( vertical = TangemTheme.dimens.spacing14, horizontal = TangemTheme.dimens.spacing16, @@ -170,13 +172,7 @@ private fun TokenItem( Spacer(modifier = Modifier.weight(1f)) - if (!token.available) { - Text( - text = stringResource(id = R.string.swapping_token_not_available), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } else if (token.addedTokenBalanceData != null) { + if (token.addedTokenBalanceData != null) { Column( horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Center, @@ -191,7 +187,11 @@ private fun TokenItem( token.addedTokenBalanceData.amountEquivalent.orEmpty() }, style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, + color = if (token.available) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.tertiary + }, ) SpacerW2() Text( @@ -220,7 +220,6 @@ private val token = TokenToSelectState.TokenToSelect( id = "", name = "USDC", symbol = "USDC", - isNative = false, addedTokenBalanceData = TokenBalanceData( amount = "15 000 $", amountEquivalent = "15 000 " + @@ -230,7 +229,7 @@ private val token = TokenToSelectState.TokenToSelect( ) private val title = TokenToSelectState.Title( - title = "MY TOKENS", + title = stringReference("MY TOKENS"), ) @Preview @@ -243,7 +242,6 @@ private fun TokenScreenPreview() { unavailableTokens = listOf(title, token, token, token).toImmutableList(), onSearchEntered = {}, onTokenSelected = {}, - network = Network("Ethereum", "ETH"), ), onBack = {}, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index 65abcf814c..c9018a2345 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData import com.tangem.feature.swap.domain.models.ui.SwapStateData +import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress import com.tangem.feature.swap.domain.models.ui.TxFee data class SwapProcessDataState( @@ -20,4 +21,5 @@ data class SwapProcessDataState( val approveDataModel: RequestApproveStateData? = null, val swapDataModel: SwapStateData? = null, val selectedFee: TxFee? = null, + val tokensDataState: TokensDataStateExpress? = null, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 99919c05a3..0f2d531f35 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -4,9 +4,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* +import arrow.core.getOrElse import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.analytics.SwapEvents @@ -27,11 +30,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.runCatching import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json import timber.log.Timber import java.text.DecimalFormat import java.text.NumberFormat @@ -47,6 +48,7 @@ internal class SwapViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -55,9 +57,12 @@ internal class SwapViewModel @Inject constructor( private var isBalanceHidden = true + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private val stateBuilder = StateBuilder( actions = createUiActions(), isBalanceHiddenProvider = Provider { isBalanceHidden }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), ) private val inputNumberFormatter = @@ -87,7 +92,7 @@ internal class SwapViewModel @Inject constructor( derivationPath = initialCryptoCurrency.network.derivationPath.value, network = initialCryptoCurrency.network, ) - initTokens(initialCryptoCurrency) + initTokens() } override fun onCreate(owner: LifecycleOwner) { @@ -126,7 +131,7 @@ internal class SwapViewModel @Inject constructor( } @Suppress("UnusedPrivateMember") - private fun initTokens(currency: CryptoCurrency) { + private fun initTokens() { // new flow viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { @@ -134,10 +139,11 @@ internal class SwapViewModel @Inject constructor( }.onSuccess { state -> dataState = dataState.copy( fromCryptoCurrency = initialCryptoCurrency, - toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency + toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency, + tokensDataState = state, ) - // updateTokensState(dataState = state.foundTokensState) + updateTokensState(state) startLoadingQuotes( fromToken = initialCryptoCurrency, toToken = state.toGroup.available.first().currencyStatus.currency, @@ -171,11 +177,11 @@ internal class SwapViewModel @Inject constructor( // } } - private fun updateTokensState(dataState: FoundTokensStateExpress) { + private fun updateTokensState(dataState: TokensDataStateExpress) { + val tokensDataState = if (!isOrderReversed) dataState.toGroup else dataState.fromGroup uiState = stateBuilder.addTokensToState( uiState = uiState, - dataState = dataState, - networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId), + tokensDataState = tokensDataState, ) } @@ -369,28 +375,31 @@ internal class SwapViewModel @Inject constructor( swapInteractor.searchTokens(dataState.networkId, searchQuery) } .onSuccess { - updateTokensState(it) + // updateTokensState(it) } .onFailure { } } } private fun onTokenSelect(id: String) { - val foundToken = swapInteractor.findTokenById(id) + val tokens = dataState.tokensDataState ?: return + val foundToken = tokens.toGroup.available.firstOrNull { + it.currencyStatus.currency.id.value == id + } analyticsEventHandler.send( - event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.symbol), + event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.currencyStatus?.currency?.symbol), ) if (foundToken != null) { val fromToken: CryptoCurrency val toToken: CryptoCurrency if (isOrderReversed) { - fromToken = foundToken + fromToken = foundToken.currencyStatus.currency toToken = initialCryptoCurrency } else { fromToken = initialCryptoCurrency - toToken = foundToken + toToken = foundToken.currencyStatus.currency } dataState = dataState.copy( fromCryptoCurrency = fromToken, @@ -532,6 +541,18 @@ internal class SwapViewModel @Inject constructor( ) } + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" From fc9be53caf7ba8450639aba77c7cf7ee94791267 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 13:47:40 +0200 Subject: [PATCH 032/139] Updated on 2026-08-14 --- .../datasource/api/express/ExpressApi.kt | 53 ------------------- .../api/express/TangemExpressApi.kt | 4 +- .../models/response/ExchangeQuoteResponse.kt | 14 +++++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 41 +++++++++----- .../feature/swap/domain/SwapInteractor.kt | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 27 +++++++--- .../feature/swap/domain/SwapRepository.kt | 13 +++-- .../swap/domain/models/ui/SwapState.kt | 1 + .../feature/swap/viewmodels/SwapViewModel.kt | 16 +++++- 9 files changed, 87 insertions(+), 83 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt deleted file mode 100644 index 0ed82c76d9..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/ExpressApi.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.datasource.api.express - -import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.express.models.request.AssetsRequestBody -import com.tangem.datasource.api.express.models.request.PairsRequestBody -import com.tangem.datasource.api.express.models.response.* -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Query -import java.math.BigDecimal - -/** - * Interface of Tangem Express API (new swap mechanism) - */ -@Suppress("LongParameterList") -interface ExpressApi { - - @POST("assets") - suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse> - - @POST("pairs") - suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse> - - @GET("providers") - suspend fun getProviders(): ApiResponse> - - @GET("exchange-quote") - suspend fun getExchangeQuote( - @Query("fromContractAddress") fromContractAddress: String, - @Query("fromNetwork") fromNetwork: String, - @Query("toContractAddress") toContractAddress: String, - @Query("toNetwork") toNetwork: String, - @Query("fromAmount") fromAmount: BigDecimal, - @Query("providerId") providerId: Int, - @Query("rateType") rateType: RateType, - ): ApiResponse - - @GET("exchange-data") - suspend fun getExchangeData( - @Query("fromContractAddress") fromContractAddress: String, - @Query("fromNetwork") fromNetwork: String, - @Query("toContractAddress") toContractAddress: String, - @Query("toNetwork") toNetwork: String, - @Query("fromAmount") fromAmount: BigDecimal, - @Query("providerId") providerId: Int, - @Query("rateType") rateType: RateType, - @Query("toAddress") toAddress: String, - ): ApiResponse - - @GET("exchange-result") - suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 627e70bdd0..0c1fb16cf8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -31,9 +31,9 @@ interface TangemExpressApi { @Query("fromNetwork") fromNetwork: String, @Query("toContractAddress") toContractAddress: String, @Query("toNetwork") toNetwork: String, - @Query("fromAmount") fromAmount: BigDecimal, + @Query("fromAmount") fromAmount: String, @Query("providerId") providerId: Int, - @Query("rateType") rateType: RateType, + @Query("rateType") rateType: String, ): ApiResponse @GET("exchange-data") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt index 214b4ac2d5..de2368e418 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt @@ -4,9 +4,23 @@ import com.squareup.moshi.Json import java.math.BigDecimal data class ExchangeQuoteResponse( + + @Json(name = "fromAmount") + val fromAmount: BigDecimal, + + @Json(name = "fromDecimals") + val fromDecimals: Int, + @Json(name = "toAmount") val toAmount: BigDecimal, + @Json(name = "toDecimals") + val toDecimals: Int, + @Json(name = "allowanceContract") val allowanceContract: String?, + + @Json(name = "minAmount") + val minAmount: BigDecimal, + ) \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 9eb5bedf15..ff7b2277ed 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -20,13 +20,16 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.mapErrors import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.withContext +import retrofit2.http.Query import java.math.BigDecimal +import java.util.Locale import javax.inject.Inject import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo @@ -127,21 +130,33 @@ internal class SwapRepositoryImpl @Inject constructor( } override suspend fun findBestQuote( - networkId: String, - fromTokenAddress: String, - toTokenAddress: String, - amount: String, + fromContractAddress: String, + fromNetwork: String, + toContractAddress: String, + toNetwork: String, + fromAmount: String, + providerId: Int, + rateType: RateType, ): AggregatedSwapDataModel { return withContext(coroutineDispatcher.io) { try { - val response = oneInchErrorsHandler.handleOneInchResponse( - getOneInchApi(networkId).quote( - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - amount = amount, - ), + val response = tangemExpressApi.getExchangeQuote( + fromContractAddress = fromContractAddress, + fromNetwork = fromNetwork, + toContractAddress = toContractAddress, + toNetwork = toNetwork, + fromAmount = fromAmount, + providerId = providerId, + rateType = rateType.name.lowercase() + ).getOrThrow() + AggregatedSwapDataModel( + dataModel = QuoteModel( + SwapAmount( + value = response.toAmount, + decimals = response.toDecimals + ) + ) ) - AggregatedSwapDataModel(dataModel = quotesConverter.convert(response)) } catch (ex: OneIncResponseException) { AggregatedSwapDataModel(null, mapErrors(ex.data.description)) } @@ -263,7 +278,7 @@ internal class SwapRepositoryImpl @Inject constructor( fromNetwork: String, toContractAddress: String, toNetwork: String, - fromAmount: BigDecimal, + fromAmount: String, providerId: Int, rateType: RateType ): ExchangeQuote { @@ -274,7 +289,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork, fromAmount, providerId, - rateTypeConverter.convertBack(rateType) + rateType.name.lowercase() ).getOrThrow() return ExchangeQuote( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 2757583249..4db2ed600b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -66,6 +66,7 @@ interface SwapInteractor { networkId: String, fromToken: CryptoCurrency, toToken: CryptoCurrency, + providers: List, amountToSwap: String, selectedFee: FeeType = FeeType.NORMAL, ): SwapState diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 738424d73c..aaa47663ff 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -274,6 +274,7 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, fromToken: CryptoCurrency, toToken: CryptoCurrency, + providers: List, amountToSwap: String, selectedFee: FeeType, ): SwapState { @@ -311,6 +312,7 @@ internal class SwapInteractorImpl @Inject constructor( toToken = toToken, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + providers = providers ) } } @@ -518,14 +520,23 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, fromToken: CryptoCurrency, toToken: CryptoCurrency, + providers: List, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, ): SwapState { repository.findBestQuote( - networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - amount = amount.toStringWithRightOffset(), + fromContractAddress = fromToken.getContractAddress(), + fromNetwork = fromToken.network.backendId, + toContractAddress = toToken.getContractAddress(), + toNetwork = toToken.network.backendId, + fromAmount = amount.toStringWithRightOffset(), + providerId = providers[0].providerId, + rateType = RateType.FLOAT + + // networkId = networkId, + // fromTokenAddress = fromTokenAddress, + // toTokenAddress = toTokenAddress, + // amount = amount.toStringWithRightOffset(), ).let { quotes -> val quoteDataModel = quotes.dataModel if (quoteDataModel != null) { @@ -668,16 +679,16 @@ internal class SwapInteractorImpl @Inject constructor( tokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: ZERO_BALANCE, tokenFiatBalance = toTokenAmount.value.toFiatString( - rateValue = rates[toToken.id.value]?.toBigDecimal() ?: BigDecimal.ZERO, + rateValue = rates[toToken.network.backendId]?.toBigDecimal() ?: BigDecimal.ZERO, fiatCurrencyName = appCurrency.symbol, formatWithSpaces = true, ), ), priceImpact = calculatePriceImpact( fromTokenAmount = fromTokenAmount.value, - fromRate = rates[fromToken.id.value] ?: 0.0, + fromRate = rates[fromToken.network.backendId] ?: 0.0, toTokenAmount = toTokenAmount.value, - toRate = rates[toToken.id.value] ?: 0.0, + toRate = rates[toToken.network.backendId] ?: 0.0, ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), swapDataModel = swapStateData, @@ -819,7 +830,7 @@ internal class SwapInteractorImpl @Inject constructor( private fun getTokenAddress(currency: CryptoCurrency): String { return when (currency) { is CryptoCurrency.Coin -> { - DEFAULT_BLOCKCHAIN_INCH_ADDRESS + "0" } is CryptoCurrency.Token -> { currency.contractAddress diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 8942e75161..2e0a8cbf0f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -15,10 +15,13 @@ interface SwapRepository { suspend fun getExchangeableTokens(networkId: String): List suspend fun findBestQuote( - networkId: String, - fromTokenAddress: String, - toTokenAddress: String, - amount: String, + fromContractAddress: String, + fromNetwork: String, + toContractAddress: String, + toNetwork: String, + fromAmount: String, + providerId: Int, + rateType: RateType, ): AggregatedSwapDataModel /** @@ -67,7 +70,7 @@ interface SwapRepository { fromNetwork: String, toContractAddress: String, toNetwork: String, - fromAmount: BigDecimal, + fromAmount: String, providerId: Int, rateType: RateType, ) : ExchangeQuote diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 28809bc066..14610d708f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -9,6 +9,7 @@ import java.math.BigDecimal sealed interface SwapState { data class QuotesLoadedState( + //add map < Provider ID, Swap state data> val fromTokenInfo: TokenSwapInfo, val toTokenInfo: TokenSwapInfo, val priceImpact: Float, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 99919c05a3..8af439d54f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -13,6 +13,8 @@ import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.domain.PermissionOptions +import com.tangem.feature.swap.domain.models.domain.RateType +import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.SwapPermissionState @@ -138,10 +140,12 @@ internal class SwapViewModel @Inject constructor( ) // updateTokensState(dataState = state.foundTokensState) + val toToken = state.toGroup.available.first() startLoadingQuotes( fromToken = initialCryptoCurrency, - toToken = state.toGroup.available.first().currencyStatus.currency, + toToken = toToken.currencyStatus.currency, amount = lastAmount.value, + toProvidersList = toToken.providers ) }.onFailure { Timber.tag(loggingTag).e(it) @@ -179,7 +183,12 @@ internal class SwapViewModel @Inject constructor( ) } - private fun startLoadingQuotes(fromToken: CryptoCurrency, toToken: CryptoCurrency, amount: String) { + private fun startLoadingQuotes( + fromToken: CryptoCurrency, + toToken: CryptoCurrency, + amount: String, + toProvidersList: List = listOf(SwapProvider(1, listOf(RateType.FLOAT))), + ) { singleTaskScheduler.cancelTask() uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, initialCryptoCurrency.id.value) singleTaskScheduler.scheduleTask( @@ -188,6 +197,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromToken, toToken = toToken, amount = amount, + toProvidersList = toProvidersList ), ) } @@ -205,6 +215,7 @@ internal class SwapViewModel @Inject constructor( fromToken: CryptoCurrency, toToken: CryptoCurrency, amount: String, + toProvidersList: List, ): PeriodicTask { return PeriodicTask( UPDATE_DELAY, @@ -220,6 +231,7 @@ internal class SwapViewModel @Inject constructor( networkId = dataState.networkId, fromToken = fromToken, toToken = toToken, + providers = toProvidersList, amountToSwap = amount, selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) From 0e55f23e8e380e1f717496dc566d7f21b7fd0685 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 17:44:15 +0200 Subject: [PATCH 033/139] Updated on 2026-08-14 --- .../models/response/ExchangeQuoteResponse.kt | 4 ++-- .../tangem/feature/swap/SwapRepositoryImpl.kt | 17 ++++++----------- .../swap/converters/SwapPairInfoConverter.kt | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 18 ++++++++---------- .../feature/swap/domain/SwapRepository.kt | 3 ++- .../swap/domain/models/domain/ExchangeQuote.kt | 6 ++---- .../feature/swap/domain/models/ui/SwapState.kt | 2 +- .../feature/swap/viewmodels/SwapViewModel.kt | 4 ++-- 8 files changed, 23 insertions(+), 32 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt index de2368e418..e643811822 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt @@ -6,13 +6,13 @@ import java.math.BigDecimal data class ExchangeQuoteResponse( @Json(name = "fromAmount") - val fromAmount: BigDecimal, + val fromAmount: String, @Json(name = "fromDecimals") val fromDecimals: Int, @Json(name = "toAmount") - val toAmount: BigDecimal, + val toAmount: String, @Json(name = "toDecimals") val toDecimals: Int, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 8d9f1918ab..0f8e561982 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -20,16 +20,14 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.SwapRepository -import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.mapErrors import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.withContext -import retrofit2.http.Query import java.math.BigDecimal -import java.util.Locale import javax.inject.Inject import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo @@ -147,15 +145,12 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork = toNetwork, fromAmount = fromAmount, providerId = providerId, - rateType = rateType.name.lowercase() + rateType = rateType.name.lowercase(), ).getOrThrow() AggregatedSwapDataModel( dataModel = QuoteModel( - SwapAmount( - value = response.toAmount, - decimals = response.toDecimals - ) - ) + toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals), + ), ) } catch (ex: OneIncResponseException) { AggregatedSwapDataModel(null, mapErrors(ex.data.description)) @@ -280,7 +275,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork: String, fromAmount: String, providerId: Int, - rateType: RateType + rateType: RateType, ): ExchangeQuote { val response = tangemExpressApi.getExchangeQuote( fromContractAddress, @@ -289,7 +284,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork, fromAmount, providerId, - rateType.name.lowercase() + rateType.name.lowercase(), ).getOrThrow() return ExchangeQuote( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index e1e48fc630..c205bec15e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -33,5 +33,4 @@ class SwapPairInfoConverter : Converter { rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) }, ) } - } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 68e8846de4..88f790c9c3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -300,14 +300,12 @@ internal class SwapInteractorImpl @Inject constructor( } else { loadQuoteData( networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, amount = amount, fromToken = fromToken, toToken = toToken, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - providers = providers + providers = providers, ) } } @@ -510,8 +508,6 @@ internal class SwapInteractorImpl @Inject constructor( @Suppress("LongParameterList") private suspend fun loadQuoteData( networkId: String, - fromTokenAddress: String, - toTokenAddress: String, amount: SwapAmount, fromToken: CryptoCurrency, toToken: CryptoCurrency, @@ -520,13 +516,13 @@ internal class SwapInteractorImpl @Inject constructor( isBalanceWithoutFeeEnough: Boolean, ): SwapState { repository.findBestQuote( - fromContractAddress = fromToken.getContractAddress(), + fromContractAddress = fromToken.getContractAddress(), fromNetwork = fromToken.network.backendId, toContractAddress = toToken.getContractAddress(), toNetwork = toToken.network.backendId, fromAmount = amount.toStringWithRightOffset(), providerId = providers[0].providerId, - rateType = RateType.FLOAT + rateType = RateType.FLOAT, // networkId = networkId, // fromTokenAddress = fromTokenAddress, @@ -653,7 +649,10 @@ internal class SwapInteractorImpl @Inject constructor( ): SwapState.QuotesLoadedState { val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) - val rates = repository.getRates(appCurrency.code, listOf(fromToken.id.value, toToken.id.value, nativeToken.id)) + val rates = repository.getRates( + appCurrency.code, + listOf(fromToken.network.backendId, toToken.network.backendId, nativeToken.id), + ) val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol) val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol) return SwapState.QuotesLoadedState( @@ -663,7 +662,7 @@ internal class SwapInteractorImpl @Inject constructor( tokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } ?: ZERO_BALANCE, tokenFiatBalance = fromTokenAmount.value.toFiatString( - rateValue = rates[fromToken.id.value]?.toBigDecimal() ?: BigDecimal.ZERO, + rateValue = rates[fromToken.network.backendId]?.toBigDecimal() ?: BigDecimal.ZERO, fiatCurrencyName = appCurrency.symbol, formatWithSpaces = true, ), @@ -899,7 +898,6 @@ internal class SwapInteractorImpl @Inject constructor( companion object { private const val DEFAULT_SLIPPAGE = 2 private const val ZERO_BALANCE = "0" - private const val DEFAULT_BLOCKCHAIN_INCH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" @Suppress("UnusedPrivateMember") private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.0 // if need to increase fee when check isEnough diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index d09b227b41..4ba2f42f83 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -14,6 +14,7 @@ interface SwapRepository { suspend fun getExchangeableTokens(networkId: String): List + @Suppress("LongParameterList") suspend fun findBestQuote( fromContractAddress: String, fromNetwork: String, @@ -74,5 +75,5 @@ interface SwapRepository { fromAmount: String, providerId: Int, rateType: RateType, - ) : ExchangeQuote + ): ExchangeQuote } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt index d087b97955..41d7826e07 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt @@ -1,8 +1,6 @@ package com.tangem.feature.swap.domain.models.domain -import java.math.BigDecimal - data class ExchangeQuote( - val toAmount: BigDecimal, - val allowanceContract: String? + val toAmount: String, + val allowanceContract: String?, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 14610d708f..b1e20ec216 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -9,7 +9,7 @@ import java.math.BigDecimal sealed interface SwapState { data class QuotesLoadedState( - //add map < Provider ID, Swap state data> + // add map < Provider ID, Swap state data> val fromTokenInfo: TokenSwapInfo, val toTokenInfo: TokenSwapInfo, val priceImpact: Float, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 245b670cbe..fe3ff3c949 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -145,7 +145,7 @@ internal class SwapViewModel @Inject constructor( tokensDataState = state, ) - //updateTokensState(state) + // updateTokensState(state) val toToken = state.toGroup.available.first() startLoadingQuotes( @@ -204,7 +204,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromToken, toToken = toToken, amount = amount, - toProvidersList = toProvidersList + toProvidersList = toProvidersList, ), ) } From 537543636bceda85f3743b46907ad0ef7abdbb3f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 18:49:21 +0300 Subject: [PATCH 034/139] Updated on 2026-08-14 --- .../src/main/res/drawable/ic_no_token_44.xml | 10 ++ .../repository/DefaultQuotesRepository.kt | 8 ++ .../GetCryptoCurrencyStatusSyncUseCase.kt | 36 +++++ .../CurrenciesStatusesOperations.kt | 22 +++ .../tokens/repository/QuotesRepository.kt | 2 + .../swap/domain/BlockchainInteractor.kt | 3 - .../domain/DefaultBlockchainInteractor.kt | 9 -- .../feature/swap/domain/SwapInteractor.kt | 3 + .../feature/swap/domain/SwapInteractorImpl.kt | 5 + .../swap/domain/di/SwapDomainModule.kt | 8 -- features/swap/presentation/build.gradle.kts | 4 + .../swap/converters/TokensDataConverter.kt | 8 +- .../feature/swap/di/SwapPresentationModule.kt | 34 +++++ .../feature/swap/models/SwapStateHolder.kt | 38 ++++-- .../tangem/feature/swap/ui/StateBuilder.kt | 93 +++++++++++-- .../feature/swap/ui/SwapScreenContent.kt | 93 ++++++++----- .../tangem/feature/swap/ui/TransactionCard.kt | 65 ++++++++- .../swap/viewmodels/SwapProcessDataState.kt | 6 +- .../feature/swap/viewmodels/SwapViewModel.kt | 129 ++++++++++-------- 19 files changed, 431 insertions(+), 145 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_no_token_44.xml create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt diff --git a/core/ui/src/main/res/drawable/ic_no_token_44.xml b/core/ui/src/main/res/drawable/ic_no_token_44.xml new file mode 100644 index 0000000000..49e3b75b2a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_no_token_44.xml @@ -0,0 +1,10 @@ + + + diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index cdd04c62ce..7aa81fc78c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -67,6 +67,14 @@ internal class DefaultQuotesRepository( } } + override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote { + return withContext(dispatchers.io) { + val quote = quotesStore.getSync(setOf(currencyId)).firstOrNull() + requireNotNull(quote) { "Unable to get quote for $currencyId" } + quotesConverter.convert(quote) + } + } + private suspend fun fetchExpiredQuotes( currenciesIds: Set, appCurrencyId: String, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt new file mode 100644 index 0000000000..ffe6e79c3b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt @@ -0,0 +1,36 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.error.mapper.mapToTokenListError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider + +class GetCryptoCurrencyStatusSyncUseCase( + internal val currenciesRepository: CurrenciesRepository, + internal val quotesRepository: QuotesRepository, + internal val networksRepository: NetworksRepository, + internal val dispatchers: CoroutineDispatcherProvider, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + ): Either { + val operations = CurrenciesStatusesOperations( + userWalletId = userWalletId, + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + ) + + return operations.getCurrencyStatusSync(cryptoCurrencyId) + .mapLeft { error -> error.mapToTokenListError() } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 3ce6112296..a86698763b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -85,6 +85,28 @@ internal class CurrenciesStatusesOperations( } } + suspend fun getCurrencyStatusSync(cryptoCurrencyId: CryptoCurrency.ID): Either { + return either { + catch( + block = { + val currency = + currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, cryptoCurrencyId) + val quotes = quotesRepository.getQuoteSync(cryptoCurrencyId).right() + val networkStatuses = + networksRepository.getNetworkStatusesSync( + userWalletId, + setOf(currency.network), + false, + ).firstOrNull { + it.network == currency.network + }.right() + return createCurrencyStatus(currency, quotes, networkStatuses) + }, + catch = { raise(Error.DataError(it)) }, + ) + } + } + fun getCardCurrenciesStatusesFlow(): Flow>> { return flow { val nonEmptyCurrencies = recover( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index a44b1e6c99..4c38eda580 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -29,4 +29,6 @@ interface QuotesRepository { * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. */ suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set + + suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt index 229911c654..8883e0f1a3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt @@ -1,12 +1,9 @@ package com.tangem.feature.swap.domain -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.NetworkInfo interface BlockchainInteractor { - fun getTokenDecimals(token: CryptoCurrency): Int - /** * In app blockchain id, actual in blockchain sdk, not the same as networkId * diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt index cb99b58076..836f193fe3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt @@ -1,6 +1,5 @@ package com.tangem.feature.swap.domain -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.NetworkInfo import com.tangem.lib.crypto.TransactionManager import javax.inject.Inject @@ -22,12 +21,4 @@ internal class DefaultBlockchainInteractor @Inject constructor( override fun getExplorerTransactionLink(networkId: String, txAddress: String): String { return transactionManager.getExplorerTransactionLink(networkId, txAddress) } - - override fun getTokenDecimals(token: CryptoCurrency): Int { - return if (token is CryptoCurrency.Token) { - token.decimals - } else { - transactionManager.getNativeTokenDecimals(token.network.backendId) - } - } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 2757583249..073c2464d8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -2,6 +2,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* @@ -110,4 +111,6 @@ interface SwapInteractor { networkId: String, fromToken: CryptoCurrency, ): Boolean + + fun getSelectedWallet(): UserWallet? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index ec089f6df7..1cfcebfb20 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -8,6 +8,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.cache.SwapDataCache import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter @@ -93,6 +94,10 @@ internal class SwapInteractorImpl @Inject constructor( ) } + override fun getSelectedWallet(): UserWallet? { + return getSelectedWalletSyncUseCase().getOrNull() + } + private fun getToCurrenciesGroup( currency: CryptoCurrency, leastPairs: List, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 7fa93f1c70..8845c68949 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.domain.di import com.tangem.domain.tokens.GetCardTokensListUseCase -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository @@ -66,13 +65,6 @@ class SwapDomainModule { return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder) } - @SwapScope - @Provides - @Singleton - fun providesGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { - return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository) - } - @SwapScope @Provides @Singleton diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 8be06eaf61..86879ce609 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -21,6 +21,10 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) /** AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index e3eff35d4c..6f42bbd1d2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -32,13 +32,17 @@ class TokensDataConverter( availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) } .toMutableList() .apply { - this.add(0, availableTitle) + if (this.isNotEmpty()) { + this.add(0, availableTitle) + } } .toImmutableList(), unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) } .toMutableList() .apply { - this.add(0, unavailableTitle) + if (this.isNotEmpty()) { + this.add(0, unavailableTitle) + } } .toImmutableList(), onSearchEntered = onSearchEntered, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt new file mode 100644 index 0000000000..441abf4a2e --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt @@ -0,0 +1,34 @@ +package com.tangem.feature.swap.di + +import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.feature.swap.domain.* +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +class SwapPresentationModule { + + @ViewModelScoped + @Provides + fun providesGetCryptoCurrenciesUseCase( + currenciesRepository: CurrenciesRepository, + dispatcherProvider: CoroutineDispatcherProvider, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + ): GetCryptoCurrencyStatusSyncUseCase { + return GetCryptoCurrencyStatusSyncUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + dispatchers = dispatcherProvider, + ) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index ace9ee54d5..6427891e34 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -7,8 +7,8 @@ import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.feature.swap.domain.models.ui.TxFee data class SwapStateHolder( - val sendCardData: SwapCardData, - val receiveCardData: SwapCardData, + val sendCardData: SwapCardState, + val receiveCardData: SwapCardState, val networkCurrency: String, val networkId: String, val blockchainId: String, // not the same as networkId, its local id in app @@ -33,18 +33,28 @@ data class SwapStateHolder( val onCancelPermissionBottomSheet: () -> Unit = {}, ) -data class SwapCardData( - val type: TransactionCardType, - val amountEquivalent: String?, - val coinId: String?, - val amountTextFieldValue: TextFieldValue?, - val tokenIconUrl: String?, - val tokenCurrency: String, - val balance: String, - val isBalanceHidden: Boolean, - val isNotNativeToken: Boolean, - val canSelectAnotherToken: Boolean = false, -) +sealed class SwapCardState { + + data class SwapCardData( + val type: TransactionCardType, + val amountEquivalent: String?, + val coinId: String?, + val amountTextFieldValue: TextFieldValue?, + val tokenIconUrl: String?, + val tokenCurrency: String, + val balance: String, + val isBalanceHidden: Boolean, + val isNotNativeToken: Boolean, + val canSelectAnotherToken: Boolean = false, + ) : SwapCardState() + + data class Empty( + val type: TransactionCardType, + val amountEquivalent: String?, + val amountTextFieldValue: TextFieldValue?, + val canSelectAnotherToken: Boolean = false, + ) : SwapCardState() +} data class SwapButton( val enabled: Boolean, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 510502e088..2b1c60f67a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -8,9 +8,12 @@ import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.domain.NetworkInfo @@ -28,7 +31,7 @@ import kotlinx.collections.immutable.toImmutableList internal class StateBuilder( private val actions: UiActions, private val isBalanceHiddenProvider: Provider, - appCurrencyProvider: Provider, + private val appCurrencyProvider: Provider, ) { private val tokensDataConverter = TokensDataConverter( @@ -42,7 +45,7 @@ internal class StateBuilder( return SwapStateHolder( networkId = initialCurrency.network.backendId, blockchainId = networkInfo.blockchainId, - sendCardData = SwapCardData( + sendCardData = SwapCardState.SwapCardData( type = TransactionCardType.SendCard(actions.onAmountChanged, actions.onAmountSelected), amountEquivalent = null, amountTextFieldValue = null, @@ -54,7 +57,7 @@ internal class StateBuilder( balance = "", isBalanceHidden = true, ), - receiveCardData = SwapCardData( + receiveCardData = SwapCardState.SwapCardData( type = TransactionCardType.ReceiveCard(), amountEquivalent = null, tokenIconUrl = "", @@ -79,6 +82,53 @@ internal class StateBuilder( ) } + fun createNoAvailableTokensToSwapState( + uiStateHolder: SwapStateHolder, + fromToken: CryptoCurrencyStatus, + ): SwapStateHolder { + if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder + return uiStateHolder.copy( + sendCardData = SwapCardState.SwapCardData( + type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), + amountTextFieldValue = TextFieldValue( + text = "0", + ), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, + coinId = uiStateHolder.sendCardData.coinId, + isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, + tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, + canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, + balance = fromToken.getFormattedAmount(), + isBalanceHidden = isBalanceHiddenProvider(), + ), + receiveCardData = SwapCardState.Empty( + type = TransactionCardType.ReceiveCard(), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + amountTextFieldValue = TextFieldValue( + text = "0", + ), + canSelectAnotherToken = true, + ), + warnings = listOf( + SwapWarning.NoAvailableTokensToSwap( + notificationConfig = NotificationConfig( + title = stringReference("No tokens"), + subtitle = stringReference("Swap tokens not available"), + iconResId = R.drawable.ic_alert_24, + ), + ), + ), + fee = FeeState.Empty, + swapButton = SwapButton( + enabled = false, + loading = false, + onClick = { }, + ), + updateInProgress = false, + ) + } + fun createQuotesLoadingState( uiStateHolder: SwapStateHolder, fromToken: CryptoCurrency, @@ -87,8 +137,10 @@ internal class StateBuilder( ): SwapStateHolder { val canSelectSendToken = mainTokenId != fromToken.id.value // TODO look at id matching val canSelectReceiveToken = mainTokenId != toToken.id.value // TODO look at id matching + if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder + if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder return uiStateHolder.copy( - sendCardData = SwapCardData( + sendCardData = SwapCardState.SwapCardData( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = null, @@ -100,7 +152,7 @@ internal class StateBuilder( balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "", isBalanceHidden = isBalanceHiddenProvider(), ), - receiveCardData = SwapCardData( + receiveCardData = SwapCardState.SwapCardData( type = TransactionCardType.ReceiveCard(), amountTextFieldValue = null, amountEquivalent = null, @@ -128,12 +180,15 @@ internal class StateBuilder( * @param onFeeSetup callback for reset fee after auto update * @return updated whole screen state */ + @Suppress("LongMethod") fun createQuotesLoadedState( uiStateHolder: SwapStateHolder, quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, onFeeSetup: (TxFee) -> Unit, ): SwapStateHolder { + if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder + if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder val warnings = mutableListOf() if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && quoteModel.preparedSwapConfigState.isFeeEnough && @@ -159,7 +214,7 @@ internal class StateBuilder( } val feeState = createFeeState(quoteModel, uiStateHolder, onFeeSetup) return uiStateHolder.copy( - sendCardData = SwapCardData( + sendCardData = SwapCardState.SwapCardData( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = quoteModel.fromTokenInfo.tokenFiatBalance, @@ -171,7 +226,7 @@ internal class StateBuilder( balance = quoteModel.fromTokenInfo.tokenWalletBalance, isBalanceHidden = isBalanceHiddenProvider(), ), - receiveCardData = SwapCardData( + receiveCardData = SwapCardState.SwapCardData( type = TransactionCardType.ReceiveCard(), amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()), amountEquivalent = quoteModel.toTokenInfo.tokenFiatBalance, @@ -208,8 +263,10 @@ internal class StateBuilder( uiStateHolder: SwapStateHolder, emptyAmountState: SwapState.EmptyAmountState, ): SwapStateHolder { + if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder + if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder return uiStateHolder.copy( - sendCardData = SwapCardData( + sendCardData = SwapCardState.SwapCardData( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = emptyAmountState.zeroAmountEquivalent, @@ -221,7 +278,7 @@ internal class StateBuilder( balance = emptyAmountState.fromTokenWalletBalance, isBalanceHidden = isBalanceHiddenProvider(), ), - receiveCardData = SwapCardData( + receiveCardData = SwapCardState.SwapCardData( type = TransactionCardType.ReceiveCard(), amountTextFieldValue = TextFieldValue("0"), amountEquivalent = emptyAmountState.zeroAmountEquivalent, @@ -268,6 +325,7 @@ internal class StateBuilder( } fun updateSwapAmount(uiState: SwapStateHolder, amount: String): SwapStateHolder { + if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState return uiState.copy( sendCardData = uiState.sendCardData.copy( amountTextFieldValue = TextFieldValue( @@ -279,6 +337,8 @@ internal class StateBuilder( } fun updateBalanceHiddenState(uiState: SwapStateHolder, isBalanceHidden: Boolean): SwapStateHolder { + if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState + if (uiState.receiveCardData !is SwapCardState.SwapCardData) return uiState val patchedSendCardData = uiState.sendCardData.copy( isBalanceHidden = isBalanceHidden, ) @@ -666,11 +726,26 @@ internal class StateBuilder( return "$firstAddressPart...$secondAddressPart" } + private fun CryptoCurrencyStatus.getFormattedAmount(): String { + val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN + + return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) + } + + @Suppress("UnusedPrivateMember") + private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { + val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN + val appCurrency = appCurrencyProvider() + + return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) + } + private companion object { const val ADDRESS_MIN_LENGTH = 11 const val ADDRESS_FIRST_PART_LENGTH = 7 const val ADDRESS_SECOND_PART_LENGTH = 4 private const val PRICE_IMPACT_THRESHOLD = 0.1 private const val HUNDRED_PERCENTS = 100 + private const val UNKNOWN_AMOUNT_SIGN = "—" } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 1ea66c1e90..153e3cd1b4 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -145,48 +145,24 @@ private fun MainInfo(state: SwapStateHolder) { } val (topCard, bottomCard, button) = createRefs() val priceImpactWarning = state.warnings.filterIsInstance().firstOrNull() - TransactionCard( - type = state.sendCardData.type, - balance = if (state.sendCardData.isBalanceHidden) { - STARS - } else { - state.sendCardData.balance - }, - textFieldValue = state.sendCardData.amountTextFieldValue, - amountEquivalent = state.sendCardData.amountEquivalent, - tokenIconUrl = state.sendCardData.tokenIconUrl ?: "", - tokenCurrency = state.sendCardData.tokenCurrency, - priceImpact = priceImpactWarning, - networkIconRes = if (state.sendCardData.isNotNativeToken) networkIconRes else null, - iconPlaceholder = state.sendCardData.coinId?.let { - getActiveIconResByCoinId(it) - }, - onChangeTokenClick = if (state.sendCardData.canSelectAnotherToken) state.onSelectTokenClick else null, + TransactionCardData( + priceImpactWarning = priceImpactWarning, + networkIconRes = networkIconRes, + swapCardState = state.sendCardData, modifier = Modifier.constrainAs(topCard) { top.linkTo(parent.top) }, + onSelectTokenClick = state.onSelectTokenClick, ) val marginCard = TangemTheme.dimens.spacing16 - TransactionCard( - type = state.receiveCardData.type, - balance = if (state.receiveCardData.isBalanceHidden) STARS else state.receiveCardData.balance, - textFieldValue = state.receiveCardData.amountTextFieldValue, - amountEquivalent = state.receiveCardData.amountEquivalent, - tokenIconUrl = state.receiveCardData.tokenIconUrl ?: "", - tokenCurrency = state.receiveCardData.tokenCurrency, - priceImpact = priceImpactWarning, - networkIconRes = if (state.receiveCardData.isNotNativeToken) networkIconRes else null, - iconPlaceholder = state.receiveCardData.coinId?.let { - getActiveIconResByCoinId(it) - }, - onChangeTokenClick = if (state.receiveCardData.canSelectAnotherToken) { - state.onSelectTokenClick - } else { - null - }, + TransactionCardData( + priceImpactWarning = priceImpactWarning, + networkIconRes = networkIconRes, + swapCardState = state.receiveCardData, modifier = Modifier.constrainAs(bottomCard) { top.linkTo(topCard.bottom, margin = marginCard) }, + onSelectTokenClick = state.onSelectTokenClick, ) val marginButton = TangemTheme.dimens.spacing32 SwapButton( @@ -200,6 +176,48 @@ private fun MainInfo(state: SwapStateHolder) { } } +@Composable +private fun TransactionCardData( + priceImpactWarning: SwapWarning.HighPriceImpact?, + networkIconRes: Int?, + swapCardState: SwapCardState, + onSelectTokenClick: (() -> Unit)?, + modifier: Modifier = Modifier, +) { + when (swapCardState) { + is SwapCardState.Empty -> { + TransactionCardEmpty( + type = swapCardState.type, + amountEquivalent = swapCardState.amountEquivalent, + textFieldValue = swapCardState.amountTextFieldValue, + onChangeTokenClick = if (swapCardState.canSelectAnotherToken) onSelectTokenClick else null, + modifier = modifier, + ) + } + is SwapCardState.SwapCardData -> { + TransactionCard( + type = swapCardState.type, + balance = if (swapCardState.isBalanceHidden) { + STARS + } else { + swapCardState.balance + }, + textFieldValue = swapCardState.amountTextFieldValue, + amountEquivalent = swapCardState.amountEquivalent, + tokenIconUrl = swapCardState.tokenIconUrl ?: "", + tokenCurrency = swapCardState.tokenCurrency, + priceImpact = priceImpactWarning, + networkIconRes = if (swapCardState.isNotNativeToken) networkIconRes else null, + iconPlaceholder = swapCardState.coinId?.let { + getActiveIconResByCoinId(it) + }, + onChangeTokenClick = if (swapCardState.canSelectAnotherToken) onSelectTokenClick else null, + modifier = modifier, + ) + } + } +} + @OptIn(ExperimentalMaterialApi::class) @Composable private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { @@ -268,7 +286,8 @@ private fun FeeItem(feeState: FeeState, currency: String) { } } is FeeState.Empty -> { - SmallInfoCard(startText = titleString, endText = "") + // show nothing + // SmallInfoCard(startText = titleString, endText = "") } } } @@ -355,7 +374,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U // region preview -private val sendCard = SwapCardData( +private val sendCard = SwapCardState.SwapCardData( type = TransactionCardType.SendCard({}) {}, amountTextFieldValue = TextFieldValue(), amountEquivalent = "1 000 000", @@ -368,7 +387,7 @@ private val sendCard = SwapCardData( isBalanceHidden = false, ) -private val receiveCard = SwapCardData( +private val receiveCard = SwapCardState.SwapCardData( type = TransactionCardType.ReceiveCard(), amountTextFieldValue = TextFieldValue(), amountEquivalent = "1 000 000", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 3cbd35a23f..d5996c36a5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -68,7 +68,7 @@ fun TransactionCard( verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.Start, ) { - Header(balance = balance, type = type) + Header(balance = stringResource(R.string.common_balance, balance), type = type) Content( type = type, @@ -106,6 +106,67 @@ fun TransactionCard( } } +@Composable +fun TransactionCardEmpty( + type: TransactionCardType, + amountEquivalent: String?, + textFieldValue: TextFieldValue?, + modifier: Modifier = Modifier, + onChangeTokenClick: (() -> Unit)? = null, +) { + Card( + shape = RoundedCornerShape(TangemTheme.dimens.radius12), + backgroundColor = TangemTheme.colors.background.primary, + elevation = TangemTheme.dimens.elevation2, + modifier = modifier, + ) { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start, + ) { + Header( + balance = stringResource(id = R.string.swapping_token_not_available), + type = type, + ) + + Content( + type = type, + amountEquivalent = amountEquivalent, + textFieldValue = textFieldValue, + priceImpact = null, + ) + } + + Box(modifier = Modifier.align(Alignment.BottomEnd)) { + Token( + tokenIconUrl = "", + tokenCurrency = "", + iconPlaceholder = R.drawable.ic_no_token_44, + ) + } + + if (onChangeTokenClick != null) { + Box(modifier = Modifier.align(Alignment.CenterEnd)) { + ChangeTokenSelector() + } + Box( + Modifier + .align(Alignment.CenterEnd) + .height(TangemTheme.dimens.size116) + .width(TangemTheme.dimens.size102) + .clickable( + indication = rememberRipple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + ) { onChangeTokenClick() }, + ) + } + } + } +} + @Composable private fun Header(type: TransactionCardType, balance: String, modifier: Modifier = Modifier) { Row( @@ -134,7 +195,7 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie SpacerW16() if (balance.isNotBlank()) { Text( - text = stringResource(R.string.common_balance, balance), + text = balance, color = TangemTheme.colors.text.tertiary, style = MaterialTheme.typography.body2, modifier = Modifier diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index c9018a2345..9c756453d6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.viewmodels -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData import com.tangem.feature.swap.domain.models.ui.SwapStateData @@ -14,8 +14,8 @@ data class SwapProcessDataState( val fromCurrency: Currency? = null, @Deprecated("used in old swap mechanism") val toCurrency: Currency? = null, - val fromCryptoCurrency: CryptoCurrency? = null, - val toCryptoCurrency: CryptoCurrency? = null, + val fromCryptoCurrency: CryptoCurrencyStatus? = null, + val toCryptoCurrency: CryptoCurrencyStatus? = null, // Amount from input val amount: String? = null, val approveDataModel: RequestApproveStateData? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 0f2d531f35..68b7f39e22 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -11,7 +11,9 @@ import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor @@ -49,11 +51,13 @@ internal class SwapViewModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { private val initialCryptoCurrency: CryptoCurrency = savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] ?: error("no expected parameter CryptoCurrency found`") + private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus private var isBalanceHidden = true @@ -88,11 +92,20 @@ internal class SwapViewModel @Inject constructor( get() = swapRouter.currentScreen init { - swapInteractor.initDerivationPathAndNetwork( - derivationPath = initialCryptoCurrency.network.derivationPath.value, - network = initialCryptoCurrency.network, - ) - initTokens() + viewModelScope.launch(dispatchers.io) { + swapInteractor.getSelectedWallet()?.let { + initialCryptoCurrencyStatus = + requireNotNull(getCryptoCurrencyStatusUseCase(it.walletId, initialCryptoCurrency.id).getOrNull()) { + "Failed to get initial crypto currency status" + } + + swapInteractor.initDerivationPathAndNetwork( + derivationPath = initialCryptoCurrency.network.derivationPath.value, + network = initialCryptoCurrency.network, + ) + initTokens() + } + } } override fun onCreate(owner: LifecycleOwner) { @@ -137,44 +150,38 @@ internal class SwapViewModel @Inject constructor( runCatching(dispatchers.io) { swapInteractor.getTokensDataState(initialCryptoCurrency) }.onSuccess { state -> - dataState = dataState.copy( - fromCryptoCurrency = initialCryptoCurrency, - toCryptoCurrency = state.toGroup.available.first().currencyStatus.currency, - tokensDataState = state, - ) - updateTokensState(state) - startLoadingQuotes( - fromToken = initialCryptoCurrency, - toToken = state.toGroup.available.first().currencyStatus.currency, - amount = lastAmount.value, - ) + applyInitialTokenChoice(state, selectInitialCurrencyToSwap(state)) }.onFailure { Timber.tag(loggingTag).e(it) } } + } - // old flow - // viewModelScope.launch(dispatchers.main) { - // runCatching(dispatchers.io) { - // swapInteractor.initTokensToSwap(currency) - // } - // .onSuccess { state -> - // // dataState = dataState.copy( - // // fromCurrency = state.preselectTokens.fromToken, - // // toCurrency = state.preselectTokens.toToken, - // // ) - // // updateTokensState(dataState = state.foundTokensState) - // // startLoadingQuotes( - // // fromToken = state.preselectTokens.fromToken, - // // toToken = state.preselectTokens.toToken, - // // amount = lastAmount.value, - // // ) - // } - // .onFailure { - // Timber.tag(loggingTag).e(it) - // } - // } + private fun applyInitialTokenChoice(state: TokensDataStateExpress, selectedCurrency: CryptoCurrencyStatus?) { + val fromCurrencyStatus = initialCryptoCurrencyStatus + dataState = dataState.copy( + fromCryptoCurrency = fromCurrencyStatus, + toCryptoCurrency = selectedCurrency, + tokensDataState = state, + ) + if (selectedCurrency == null) { + uiState = stateBuilder.createNoAvailableTokensToSwapState( + uiStateHolder = uiState, + fromToken = fromCurrencyStatus, + ) + } else { + startLoadingQuotes( + fromToken = fromCurrencyStatus.currency, + toToken = selectedCurrency.currency, + amount = lastAmount.value, + ) + } + } + + private fun selectInitialCurrencyToSwap(state: TokensDataStateExpress): CryptoCurrencyStatus? { + // todo add algorithm to select initial currency + return state.toGroup.available.firstOrNull()?.currencyStatus } private fun updateTokensState(dataState: TokensDataStateExpress) { @@ -203,7 +210,7 @@ internal class SwapViewModel @Inject constructor( val toCurrency = dataState.toCryptoCurrency val amount = dataState.amount if (fromCurrency != null && toCurrency != null && amount != null) { - startLoadingQuotes(fromCurrency, toCurrency, amount) + startLoadingQuotes(fromCurrency.currency, toCurrency.currency, amount) } } @@ -285,8 +292,8 @@ internal class SwapViewModel @Inject constructor( swapInteractor.onSwap( networkId = dataState.networkId, swapStateData = requireNotNull(dataState.swapDataModel), - currencyToSend = requireNotNull(dataState.fromCryptoCurrency), - currencyToGet = requireNotNull(dataState.toCryptoCurrency), + currencyToSend = requireNotNull(dataState.fromCryptoCurrency?.currency), + currencyToGet = requireNotNull(dataState.toCryptoCurrency?.currency), amountToSwap = requireNotNull(dataState.amount), fee = requireNotNull(dataState.selectedFee), ) @@ -335,10 +342,10 @@ internal class SwapViewModel @Inject constructor( approveData = requireNotNull(dataState.approveDataModel) { "dataState.approveDataModel might not be null" }, - forTokenContractAddress = (dataState.fromCryptoCurrency as? CryptoCurrency.Token) + forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token) ?.contractAddress ?: "", - fromToken = requireNotNull(dataState.fromCryptoCurrency) { + fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) { "dataState.fromCurrency might not be null" }, approveType = requireNotNull(uiState.permissionState as? SwapPermissionState.ReadyForRequest) { @@ -384,28 +391,34 @@ internal class SwapViewModel @Inject constructor( private fun onTokenSelect(id: String) { val tokens = dataState.tokensDataState ?: return - val foundToken = tokens.toGroup.available.firstOrNull { - it.currencyStatus.currency.id.value == id + val foundToken = if (isOrderReversed) { + tokens.fromGroup.available.firstOrNull { + it.currencyStatus.currency.id.value == id + } + } else { + tokens.toGroup.available.firstOrNull { + it.currencyStatus.currency.id.value == id + } } analyticsEventHandler.send( event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.currencyStatus?.currency?.symbol), ) if (foundToken != null) { - val fromToken: CryptoCurrency - val toToken: CryptoCurrency + val fromToken: CryptoCurrencyStatus + val toToken: CryptoCurrencyStatus if (isOrderReversed) { - fromToken = foundToken.currencyStatus.currency - toToken = initialCryptoCurrency + fromToken = foundToken.currencyStatus + toToken = initialCryptoCurrencyStatus } else { - fromToken = initialCryptoCurrency - toToken = foundToken.currencyStatus.currency + fromToken = initialCryptoCurrencyStatus + toToken = foundToken.currencyStatus } dataState = dataState.copy( fromCryptoCurrency = fromToken, toCryptoCurrency = toToken, ) - startLoadingQuotes(fromToken, toToken, lastAmount.value) + startLoadingQuotes(fromToken.currency, toToken.currency, lastAmount.value) swapRouter.openScreen(SwapNavScreen.Main) } } @@ -419,13 +432,13 @@ internal class SwapViewModel @Inject constructor( toCryptoCurrency = newToToken, ) isOrderReversed = !isOrderReversed - val decimals = blockchainInteractor.getTokenDecimals(newFromToken) + val decimals = newFromToken.currency.decimals lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value) uiState = stateBuilder.updateSwapAmount( uiState, inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), ) - startLoadingQuotes(newFromToken, newToToken, lastAmount.value) + startLoadingQuotes(newFromToken.currency, newToToken.currency, lastAmount.value) } } @@ -433,20 +446,20 @@ internal class SwapViewModel @Inject constructor( val fromToken = dataState.fromCryptoCurrency val toToken = dataState.toCryptoCurrency if (fromToken != null && toToken != null) { - val decimals = blockchainInteractor.getTokenDecimals(fromToken) + val decimals = fromToken.currency.decimals val cutValue = cutAmountWithDecimals(decimals, value) lastAmount.value = cutValue uiState = stateBuilder.updateSwapAmount(uiState, inputNumberFormatter.formatWithThousands(cutValue, decimals)) amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { - startLoadingQuotes(fromToken, toToken, lastAmount.value) + startLoadingQuotes(fromToken.currency, toToken.currency, lastAmount.value) } } } private fun onMaxAmountClicked() { dataState.fromCryptoCurrency?.let { - val balance = swapInteractor.getTokenBalance(initialCryptoCurrency.network.id.value, it) + val balance = swapInteractor.getTokenBalance(initialCryptoCurrency.network.id.value, it.currency) onAmountChanged(balance.formatToUIRepresentation()) } } @@ -522,7 +535,7 @@ internal class SwapViewModel @Inject constructor( dataState = dataState.copy(selectedFee = feeItem.data) val spendAmount = dataState.amount?.let { amount -> val fromToken = dataState.fromCryptoCurrency ?: return@let null - swapInteractor.getSwapAmountForToken(amount, fromToken) + swapInteractor.getSwapAmountForToken(amount, fromToken.currency) } ?: dataState.approveDataModel?.fromTokenAmount spendAmount ?: return@UiActions val fromToken = dataState.fromCryptoCurrency ?: return@UiActions @@ -531,7 +544,7 @@ internal class SwapViewModel @Inject constructor( fee = feeItem.data.feeValue, spendAmount = spendAmount, networkId = dataState.networkId, - fromToken = fromToken, + fromToken = fromToken.currency, ) uiState = stateBuilder.updateFeeSelectedItem(uiState, feeItem, isFeeEnough) } From bbf8147aa4605cc7bb3a038532ef5f8eb302f83e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Nov 2023 23:07:53 +0200 Subject: [PATCH 035/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 108 +++++++++++------- .../swap/domain/models/ui/SwapState.kt | 1 - 2 files changed, 68 insertions(+), 41 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 88f790c9c3..27d8c090a1 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -12,6 +12,7 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.cache.SwapDataCache import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.toStringWithRightOffset @@ -26,6 +27,9 @@ import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode import javax.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope @Suppress("LargeClass", "LongParameterList") internal class SwapInteractorImpl @Inject constructor( @@ -306,7 +310,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, providers = providers, - ) + ).entries.first().value // TODO } } @@ -514,46 +518,70 @@ internal class SwapInteractorImpl @Inject constructor( providers: List, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, - ): SwapState { - repository.findBestQuote( - fromContractAddress = fromToken.getContractAddress(), - fromNetwork = fromToken.network.backendId, - toContractAddress = toToken.getContractAddress(), - toNetwork = toToken.network.backendId, - fromAmount = amount.toStringWithRightOffset(), - providerId = providers[0].providerId, - rateType = RateType.FLOAT, - - // networkId = networkId, - // fromTokenAddress = fromTokenAddress, - // toTokenAddress = toTokenAddress, - // amount = amount.toStringWithRightOffset(), - ).let { quotes -> - val quoteDataModel = quotes.dataModel - if (quoteDataModel != null) { - val swapState = updateBalances( - networkId = networkId, - fromToken = fromToken, - toToken = toToken, - fromTokenAmount = amount, - toTokenAmount = quoteDataModel.toTokenAmount, - swapStateData = null, - ) - val quotesState = updatePermissionState( - networkId = networkId, - fromToken = fromToken, - swapAmount = amount, - quotesLoadedState = swapState, - ) - return quotesState.copy( - preparedSwapConfigState = quotesState.preparedSwapConfigState.copy( - isAllowedToSpend = isAllowedToSpend, - isBalanceEnough = isBalanceWithoutFeeEnough, - ), - ) - } else { - return SwapState.SwapError(quotes.error) + ): Map { + return coroutineScope { + val quoteRequests = providers.map { provider -> + async { + provider to repository.findBestQuote( + fromContractAddress = fromToken.getContractAddress(), + fromNetwork = fromToken.network.backendId, + toContractAddress = toToken.getContractAddress(), + toNetwork = toToken.network.backendId, + fromAmount = amount.toStringWithRightOffset(), + providerId = provider.providerId, + rateType = RateType.FLOAT, + ) + } } + + quoteRequests.awaitAll().map { + it.first to + getState( + quoteDataModel = it.second, + amount = amount, + fromToken = fromToken, + toToken = toToken, + networkId = networkId, + isAllowedToSpend = isAllowedToSpend, + isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough + ) + }.associate { it.first to it.second } + } + } + + private suspend fun getState( + quoteDataModel: AggregatedSwapDataModel, + amount: SwapAmount, + fromToken: CryptoCurrency, + toToken: CryptoCurrency, + networkId: String, + isAllowedToSpend: Boolean, + isBalanceWithoutFeeEnough: Boolean, + ): SwapState { + val quoteModel = quoteDataModel.dataModel + if (quoteModel != null) { + val swapState = updateBalances( + networkId = networkId, + fromToken = fromToken, + toToken = toToken, + fromTokenAmount = amount, + toTokenAmount = quoteModel.toTokenAmount, + swapStateData = null, + ) + val quotesState = updatePermissionState( + networkId = networkId, + fromToken = fromToken, + swapAmount = amount, + quotesLoadedState = swapState, + ) + return quotesState.copy( + preparedSwapConfigState = quotesState.preparedSwapConfigState.copy( + isAllowedToSpend = isAllowedToSpend, + isBalanceEnough = isBalanceWithoutFeeEnough, + ), + ) + } else { + return SwapState.SwapError(quoteDataModel.error) } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index b1e20ec216..28809bc066 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -9,7 +9,6 @@ import java.math.BigDecimal sealed interface SwapState { data class QuotesLoadedState( - // add map < Provider ID, Swap state data> val fromTokenInfo: TokenSwapInfo, val toTokenInfo: TokenSwapInfo, val priceImpact: Float, From b1b7b1bc15e20d36e6264e476aba36a2c1c02878 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Nov 2023 01:48:04 +0200 Subject: [PATCH 036/139] Updated on 2026-08-14 --- .../feature/swap/viewmodels/SwapViewModel.kt | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 9c047326e0..987762b439 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -177,6 +177,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromCurrencyStatus.currency, toToken = selectedCurrency.currency, amount = lastAmount.value, + toProvidersList = findSwapProviders(fromCurrencyStatus, selectedCurrency) ) } } @@ -198,7 +199,7 @@ internal class SwapViewModel @Inject constructor( fromToken: CryptoCurrency, toToken: CryptoCurrency, amount: String, - toProvidersList: List = listOf(SwapProvider(1, listOf(RateType.FLOAT))), + toProvidersList: List, ) { singleTaskScheduler.cancelTask() uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, initialCryptoCurrency.id.value) @@ -218,7 +219,12 @@ internal class SwapViewModel @Inject constructor( val toCurrency = dataState.toCryptoCurrency val amount = dataState.amount if (fromCurrency != null && toCurrency != null && amount != null) { - startLoadingQuotes(fromCurrency.currency, toCurrency.currency, amount) + startLoadingQuotes( + fromToken = fromCurrency.currency, + toToken = toCurrency.currency, + amount = amount, + toProvidersList = findSwapProviders(fromCurrency, toCurrency) + ) } } @@ -428,7 +434,12 @@ internal class SwapViewModel @Inject constructor( fromCryptoCurrency = fromToken, toCryptoCurrency = toToken, ) - startLoadingQuotes(fromToken.currency, toToken.currency, lastAmount.value) + startLoadingQuotes( + fromToken = fromToken.currency, + toToken = toToken.currency, + amount = lastAmount.value, + toProvidersList = findSwapProviders(fromToken, toToken) + ) swapRouter.openScreen(SwapNavScreen.Main) } } @@ -448,7 +459,12 @@ internal class SwapViewModel @Inject constructor( uiState, inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), ) - startLoadingQuotes(newFromToken.currency, newToToken.currency, lastAmount.value) + startLoadingQuotes( + fromToken = newFromToken.currency, + toToken = newToToken.currency, + amount = lastAmount.value, + toProvidersList = findSwapProviders(newFromToken, newToToken) + ) } } @@ -462,7 +478,12 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.updateSwapAmount(uiState, inputNumberFormatter.formatWithThousands(cutValue, decimals)) amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { - startLoadingQuotes(fromToken.currency, toToken.currency, lastAmount.value) + startLoadingQuotes( + fromToken = fromToken.currency, + toToken = toToken.currency, + amount = lastAmount.value, + toProvidersList = findSwapProviders(fromToken, toToken) + ) } } } @@ -576,6 +597,22 @@ internal class SwapViewModel @Inject constructor( ) } + private fun findSwapProviders(fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus): List { + val groupToFind = if (isOrderReversed) { + dataState.tokensDataState?.fromGroup + } else { + dataState.tokensDataState?.toGroup + } ?: return emptyList() + + val idToFind = if (isOrderReversed) { + fromToken.currency.id.value + } else { + toToken.currency.id.value + } + + return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers ?: emptyList() + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" From 3f6a17e340efabc804d22a375e63f1ed379d12cc Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Nov 2023 11:31:08 +0200 Subject: [PATCH 037/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractor.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 27 ++++++++++--------- .../feature/swap/viewmodels/SwapViewModel.kt | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 32e9ebdf94..c6ab0a7308 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -70,7 +70,7 @@ interface SwapInteractor { providers: List, amountToSwap: String, selectedFee: FeeType = FeeType.NORMAL, - ): SwapState + ): Map /** * Starts swap transaction, perform sign transaction diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index c851cc930a..dad225b375 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -281,11 +281,11 @@ internal class SwapInteractorImpl @Inject constructor( providers: List, amountToSwap: String, selectedFee: FeeType, - ): SwapState { + ): Map { syncWalletBalanceForTokens(networkId, listOf(fromToken, toToken)) val amountDecimal = toBigDecimalOrNull(amountToSwap) if (amountDecimal == null || amountDecimal.signum() == 0) { - return createEmptyAmountState(networkId, fromToken, toToken) + return providers.associateWith { createEmptyAmountState(networkId, fromToken, toToken) } } val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken)) val fromTokenAddress = getTokenAddress(fromToken) @@ -297,15 +297,18 @@ internal class SwapInteractorImpl @Inject constructor( } val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken, amount, null) return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { - loadSwapData( - networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - fromToken = fromToken, - toToken = toToken, - amount = amount, - selectedFee = selectedFee, - ) + // TODO + providers.associateWith { + loadSwapData( + networkId = networkId, + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + fromToken = fromToken, + toToken = toToken, + amount = amount, + selectedFee = selectedFee, + ) + } } else { loadQuoteData( networkId = networkId, @@ -315,7 +318,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, providers = providers, - ).entries.first().value // TODO + ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 987762b439..03199e51f5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -251,7 +251,7 @@ internal class SwapViewModel @Inject constructor( providers = toProvidersList, amountToSwap = amount, selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, - ) + ).entries.first().value// TODO } }, onSuccess = { swapState -> From a6f272223a8d99f8a72813d25ce713306db1e8a3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Nov 2023 16:27:08 +0300 Subject: [PATCH 038/139] Updated on 2026-08-14 --- .../api/express/TangemExpressApi.kt | 2 +- .../models/response/ExchangeProvider.kt | 8 +- .../api/express/models/response/SwapPair.kt | 2 +- .../tangem/feature/swap/SwapRepositoryImpl.kt | 17 +- .../swap/converters/SwapProviderConverter.kt | 25 +++ .../feature/swap/domain/SwapInteractor.kt | 15 +- .../feature/swap/domain/SwapInteractorImpl.kt | 154 ++++++------------ .../feature/swap/domain/SwapRepository.kt | 6 +- .../swap/domain/di/SwapDomainModule.kt | 1 - .../domain/models/domain/SwapPairLeast.kt | 12 +- .../swap/domain/models/ui/SwapState.kt | 6 +- .../feature/swap/models/SwapStateHolder.kt | 2 + .../swap/models/states/ProviderState.kt | 5 + .../tangem/feature/swap/ui/ProviderItem.kt | 15 +- .../tangem/feature/swap/ui/StateBuilder.kt | 55 ++++++- .../feature/swap/ui/SwapScreenContent.kt | 4 + .../swap/viewmodels/SwapProcessDataState.kt | 8 +- .../feature/swap/viewmodels/SwapViewModel.kt | 79 +++++---- 18 files changed, 237 insertions(+), 179 deletions(-) create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 0c1fb16cf8..2d62fb3630 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -32,7 +32,7 @@ interface TangemExpressApi { @Query("toContractAddress") toContractAddress: String, @Query("toNetwork") toNetwork: String, @Query("fromAmount") fromAmount: String, - @Query("providerId") providerId: Int, + @Query("providerId") providerId: String, @Query("rateType") rateType: String, ): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index 934d1a8497..4e2bc6d69c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -4,19 +4,19 @@ import com.squareup.moshi.Json data class ExchangeProvider( @Json(name = "id") - val id: Int, + val id: String, @Json(name = "name") val name: String, - @Json(name = "id") + @Json(name = "type") val type: ExchangeProviderType, @Json(name = "imageLarge") - val imageLargeUrl: Int, + val imageLargeUrl: String, @Json(name = "imageSmall") - val imageSmallUrl: Int, + val imageSmallUrl: String, ) enum class ExchangeProviderType { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt index 2d8b0b7cf3..f96aef7dfc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt @@ -17,7 +17,7 @@ data class SwapPair( data class SwapPairProvider( @Json(name = "providerId") - val providerId: Int, + val providerId: String, @Json(name = "rateTypes") val rateTypes: List, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 0f8e561982..00d5f2448a 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -48,6 +48,7 @@ internal class SwapRepositoryImpl @Inject constructor( private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() private val rateTypeConverter = RateTypeConverter() + private val swapProviderConverter = SwapProviderConverter() override suspend fun getPairs( initialCurrency: LeastTokenInfo, @@ -78,6 +79,18 @@ internal class SwapRepositoryImpl @Inject constructor( } } + override suspend fun getProvidersDetails(providers: Set): List { + val providersMap = providers.associateBy { it.providerId } + return tangemExpressApi.getProviders().getOrThrow().mapNotNull { + val provider = providersMap[it.id] + if (provider != null) { + swapProviderConverter.convert(it).copy(rateTypes = provider.rateTypes) + } else { + null + } + } + } + private suspend fun getPairsInternal( from: List, to: List, @@ -133,7 +146,7 @@ internal class SwapRepositoryImpl @Inject constructor( toContractAddress: String, toNetwork: String, fromAmount: String, - providerId: Int, + providerId: String, rateType: RateType, ): AggregatedSwapDataModel { return withContext(coroutineDispatcher.io) { @@ -274,7 +287,7 @@ internal class SwapRepositoryImpl @Inject constructor( toContractAddress: String, toNetwork: String, fromAmount: String, - providerId: Int, + providerId: String, rateType: RateType, ): ExchangeQuote { val response = tangemExpressApi.getExchangeQuote( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt new file mode 100644 index 0000000000..6930923128 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt @@ -0,0 +1,25 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.express.models.response.ExchangeProvider +import com.tangem.datasource.api.express.models.response.ExchangeProviderType +import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.utils.converter.Converter +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType as ExchangeProviderTypeDomain + +class SwapProviderConverter : Converter { + override fun convert(value: ExchangeProvider): SwapProvider { + return SwapProvider( + providerId = value.id, + name = value.name, + type = convertExchangeType(value.type), + imageLarge = value.imageLargeUrl, + ) + } + + private fun convertExchangeType(type: ExchangeProviderType): ExchangeProviderTypeDomain { + return when (type) { + ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX + ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX + } + } +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index c6ab0a7308..4a196f0569 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.models.SwapAmount @@ -14,16 +15,6 @@ interface SwapInteractor { fun initDerivationPathAndNetwork(derivationPath: String?, network: Network) - /** - * Init tokens to swap, load tokens list available to swap for given network - * - * @param initialCurrency currency which to swap or receive - * @return [TokensDataState] that contains info about all available to swap tokens for networkId - * and preselected tokens which initially select to swap - */ - @Deprecated("method is used in the old swap mechanism") - suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState - /** * On search token, locally search tokens in previously loaded list to swap * searching in names and symbols @@ -65,8 +56,8 @@ interface SwapInteractor { @Throws(IllegalStateException::class) suspend fun findBestQuote( networkId: String, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, providers: List, amountToSwap: String, selectedFee: FeeType = FeeType.NORMAL, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index dad225b375..1dd4dab66c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -24,13 +24,13 @@ import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.toFiatString +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode import javax.inject.Inject -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope @Suppress("LargeClass", "LongParameterList") internal class SwapInteractorImpl @Inject constructor( @@ -150,7 +150,16 @@ internal class SwapInteractorImpl @Inject constructor( initialCurrency: LeastTokenInfo, currenciesList: List, ): List { - return repository.getPairs(initialCurrency, currenciesList) + val pairs = repository.getPairs(initialCurrency, currenciesList) + val providers = pairs.flatMap { it.providers }.toSet() + val updatedProviders = repository.getProvidersDetails(providers).associateBy { it.providerId } + return pairs.map { pair -> + pair.copy( + providers = pair.providers.mapNotNull { currentProvider -> + updatedProviders[currentProvider.providerId] + }, + ) + } } @Deprecated("used in old swap mechanism") @@ -159,54 +168,6 @@ internal class SwapInteractorImpl @Inject constructor( this.network = network } - @Deprecated("used in old swap mechanism") - override suspend fun initTokensToSwap(initialCurrency: Currency): TokensDataState { - // TODO: refactor this function - val networkId = initialCurrency.networkId - val availableTokens = cache.getAvailableTokens(networkId) - val allLoadedTokens = availableTokens.ifEmpty { - val tokens = repository.getExchangeableTokens(networkId) - cache.cacheAvailableToSwapTokens(networkId, tokens) - tokens - }.filter { it.symbol != initialCurrency.symbol } - - // replace tokens in wallet tokens list with loaded same - val loadedOnWalletsMap = mutableSetOf() - val tokensInWallet = userWalletManager.getUserTokens( - networkId = networkId, - derivationPath = derivationPath, - isExcludeCustom = true, - ) - .map { token -> - allLoadedTokens.firstOrNull { it.symbol == token.symbol }?.let { - loadedOnWalletsMap.add(it.symbol) - it - } ?: TODO() - } - .filter { it.symbol != initialCurrency.symbol && allLoadedTokens.contains(it) } - val loadedTokens = allLoadedTokens - .filter { - !loadedOnWalletsMap.contains(it.symbol) - } - val tokensBalance = userWalletManager.getCurrentWalletTokensBalance(networkId, emptyList(), derivationPath) - .mapValues { SwapAmount(it.value.value, it.value.decimals) } - val appCurrency = userWalletManager.getUserAppCurrency() - val rates = repository.getRates(appCurrency.code, tokensInWallet.map { it.id }) - cache.cacheBalances(networkId, derivationPath, tokensBalance) - // cache.cacheLoadedTokens(loadedTokens.map { TokenWithBalance(it) }) - // cache.cacheInWalletTokens(getTokensWithBalance(tokensInWallet, tokensBalance, rates, appCurrency)) - return TokensDataState( - preselectTokens = PreselectTokens( - fromToken = initialCurrency, - toToken = selectToToken(initialCurrency, tokensInWallet, loadedTokens), - ), - foundTokensState = FoundTokensState( - tokensInWallet = emptyList(), // cache.getInWalletTokens(), - loadedTokens = emptyList(), // cache.getLoadedTokens(), - ), - ) - } - @Deprecated("used in old swap mechanism") override suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensStateExpress { val searchQueryLowerCase = searchQuery.lowercase() @@ -276,26 +237,26 @@ internal class SwapInteractorImpl @Inject constructor( @Deprecated("used in old swap mechanism") override suspend fun findBestQuote( networkId: String, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, providers: List, amountToSwap: String, selectedFee: FeeType, ): Map { - syncWalletBalanceForTokens(networkId, listOf(fromToken, toToken)) + syncWalletBalanceForTokens(networkId, listOf(fromToken.currency, toToken.currency)) val amountDecimal = toBigDecimalOrNull(amountToSwap) if (amountDecimal == null || amountDecimal.signum() == 0) { - return providers.associateWith { createEmptyAmountState(networkId, fromToken, toToken) } + return providers.associateWith { createEmptyAmountState(networkId, fromToken.currency, toToken.currency) } } - val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken)) - val fromTokenAddress = getTokenAddress(fromToken) - val toTokenAddress = getTokenAddress(toToken) - val isAllowedToSpend = isAllowedToSpend(networkId, fromToken, amount) + val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) + val fromTokenAddress = getTokenAddress(fromToken.currency) + val toTokenAddress = getTokenAddress(toToken.currency) + val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount) if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) transactionManager.updateWalletManager(networkId, derivationPath) } - val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken, amount, null) + val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null) return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { // TODO providers.associateWith { @@ -313,8 +274,8 @@ internal class SwapInteractorImpl @Inject constructor( loadQuoteData( networkId = networkId, amount = amount, - fromToken = fromToken, - toToken = toToken, + fromTokenStatus = fromToken, + toTokenStatus = toToken, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, providers = providers, @@ -521,12 +482,14 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun loadQuoteData( networkId: String, amount: SwapAmount, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromTokenStatus: CryptoCurrencyStatus, + toTokenStatus: CryptoCurrencyStatus, providers: List, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, ): Map { + val fromToken = fromTokenStatus.currency + val toToken = toTokenStatus.currency return coroutineScope { val quoteRequests = providers.map { provider -> async { @@ -547,11 +510,11 @@ internal class SwapInteractorImpl @Inject constructor( getState( quoteDataModel = it.second, amount = amount, - fromToken = fromToken, - toToken = toToken, + fromToken = fromTokenStatus, + toToken = toTokenStatus, networkId = networkId, isAllowedToSpend = isAllowedToSpend, - isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough + isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, ) }.associate { it.first to it.second } } @@ -560,8 +523,8 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun getState( quoteDataModel: AggregatedSwapDataModel, amount: SwapAmount, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, networkId: String, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, @@ -570,15 +533,15 @@ internal class SwapInteractorImpl @Inject constructor( if (quoteModel != null) { val swapState = updateBalances( networkId = networkId, - fromToken = fromToken, - toToken = toToken, + fromTokenStatus = fromToken, + toTokenStatus = toToken, fromTokenAmount = amount, toTokenAmount = quoteModel.toTokenAmount, swapStateData = null, ) val quotesState = updatePermissionState( networkId = networkId, - fromToken = fromToken, + fromToken = fromToken.currency, swapAmount = amount, quotesLoadedState = swapState, ) @@ -612,8 +575,8 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, fromTokenAddress: String, toTokenAddress: String, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, amount: SwapAmount, selectedFee: FeeType, ): SwapState { @@ -630,7 +593,7 @@ internal class SwapInteractorImpl @Inject constructor( val feeData = transactionManager.getFee( networkId = networkId, amountToSend = amount.value, - currencyToSend = swapCurrencyConverter.convert(fromToken), + currencyToSend = swapCurrencyConverter.convert(fromToken.currency), destinationAddress = swapData.transaction.toWalletAddress, increaseBy = INCREASE_GAS_LIMIT_BY, data = swapData.transaction.data, @@ -642,17 +605,17 @@ internal class SwapInteractorImpl @Inject constructor( FeeType.PRIORITY -> txFeeState.priorityFee.feeValue } val isBalanceIncludeFeeEnough = - isBalanceEnough(networkId, fromToken, amount, feeByPriority) + isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority) val isFeeEnough = checkFeeIsEnough( fee = feeByPriority, spendAmount = amount, networkId = networkId, - fromToken = fromToken, + fromToken = fromToken.currency, ) val swapState = updateBalances( networkId = networkId, - fromToken = fromToken, - toToken = toToken, + fromTokenStatus = fromToken, + toTokenStatus = toToken, fromTokenAmount = amount, toTokenAmount = swapData.toTokenAmount, swapStateData = SwapStateData( @@ -677,42 +640,32 @@ internal class SwapInteractorImpl @Inject constructor( @Suppress("LongParameterList") private suspend fun updateBalances( networkId: String, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromTokenStatus: CryptoCurrencyStatus, + toTokenStatus: CryptoCurrencyStatus, fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, swapStateData: SwapStateData?, ): SwapState.QuotesLoadedState { + val fromToken = fromTokenStatus.currency + val toToken = toTokenStatus.currency val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) val rates = repository.getRates( appCurrency.code, listOf(fromToken.network.backendId, toToken.network.backendId, nativeToken.id), ) - val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol) - val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol) return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( tokenAmount = fromTokenAmount, - coinId = fromToken.id.value, - tokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } - ?: ZERO_BALANCE, - tokenFiatBalance = fromTokenAmount.value.toFiatString( - rateValue = rates[fromToken.network.backendId]?.toBigDecimal() ?: BigDecimal.ZERO, - fiatCurrencyName = appCurrency.symbol, - formatWithSpaces = true, - ), + cryptoCurrencyStatus = fromTokenStatus, + amountFiat = rates[fromToken.network.backendId]?.toBigDecimal()?.multiply(fromTokenAmount.value) + ?: BigDecimal.ZERO, ), toTokenInfo = TokenSwapInfo( tokenAmount = toTokenAmount, - coinId = toToken.id.value, - tokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") } - ?: ZERO_BALANCE, - tokenFiatBalance = toTokenAmount.value.toFiatString( - rateValue = rates[toToken.network.backendId]?.toBigDecimal() ?: BigDecimal.ZERO, - fiatCurrencyName = appCurrency.symbol, - formatWithSpaces = true, - ), + cryptoCurrencyStatus = toTokenStatus, + amountFiat = rates[toToken.network.backendId]?.toBigDecimal()?.multiply(toTokenAmount.value) + ?: BigDecimal.ZERO, ), priceImpact = calculatePriceImpact( fromTokenAmount = fromTokenAmount.value, @@ -933,7 +886,6 @@ internal class SwapInteractorImpl @Inject constructor( companion object { private const val DEFAULT_SLIPPAGE = 2 - private const val ZERO_BALANCE = "0" @Suppress("UnusedPrivateMember") private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.0 // if need to increase fee when check isEnough diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 4ba2f42f83..f8664afb69 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -10,6 +10,8 @@ interface SwapRepository { suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List): List + suspend fun getProvidersDetails(providers: Set): List + suspend fun getRates(currencyId: String, tokenIds: List): Map suspend fun getExchangeableTokens(networkId: String): List @@ -21,7 +23,7 @@ interface SwapRepository { toContractAddress: String, toNetwork: String, fromAmount: String, - providerId: Int, + providerId: String, rateType: RateType, ): AggregatedSwapDataModel @@ -73,7 +75,7 @@ interface SwapRepository { toContractAddress: String, toNetwork: String, fromAmount: String, - providerId: Int, + providerId: String, rateType: RateType, ): ExchangeQuote } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 56fd4124d3..8845c68949 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.domain.di import com.tangem.domain.tokens.GetCardTokensListUseCase -import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index ee772becce..76c2c0795d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -27,10 +27,18 @@ data class CryptoCurrencySwapInfo( * @property rateTypes supported rate types */ data class SwapProvider( - val providerId: Int, - val rateTypes: List, + val providerId: String, + val rateTypes: List = emptyList(), + val name: String? = null, + val type: ExchangeProviderType? = null, + val imageLarge: String? = null, ) +enum class ExchangeProviderType { + DEX, + CEX, +} + /** * Rate type. * diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 28809bc066..0dc24830f3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.domain.models.ui +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState @@ -51,9 +52,8 @@ sealed class PermissionDataState { data class TokenSwapInfo( val tokenAmount: SwapAmount, - val coinId: String, - val tokenWalletBalance: String, - val tokenFiatBalance: String, + val amountFiat: BigDecimal, + val cryptoCurrencyStatus: CryptoCurrencyStatus, ) data class RequestApproveStateData( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 6427891e34..e166729104 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.feature.swap.domain.models.ui.TxFee +import com.tangem.feature.swap.models.states.ProviderState data class SwapStateHolder( val sendCardData: SwapCardState, @@ -16,6 +17,7 @@ data class SwapStateHolder( val warnings: List = emptyList(), val alert: SwapWarning.GenericWarning? = null, val updateInProgress: Boolean = false, + val providerState: ProviderState, val permissionState: SwapPermissionState = SwapPermissionState.Empty, val successState: SwapSuccessStateHolder? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index f78f6d6cb3..78feaf99a9 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -5,6 +5,11 @@ sealed class ProviderState { abstract val onProviderClick: ((String) -> Unit)? abstract val id: String + data class Empty( + override val id: String = "", + override val onProviderClick: ((String) -> Unit)? = null, + ) : ProviderState() + data class Loading( override val id: String = "", override val onProviderClick: ((String) -> Unit)? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 6602b25ca8..dfe32ead57 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -36,11 +36,13 @@ private val GrayscaleColorFilter: ColorFilter @Composable fun ProviderItemBlock(state: ProviderState) { - BaseContainer(state) { - ProviderItem( - state = state, - modifier = Modifier.align(Alignment.CenterStart), - ) + if (state !is ProviderState.Empty) { + BaseContainer(state) { + ProviderItem( + state = state, + modifier = Modifier.align(Alignment.CenterStart), + ) + } } } @@ -65,6 +67,9 @@ fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected modifier = modifier, ) } + is ProviderState.Empty -> { + // do nothing + } } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 2b1c60f67a..4cbd99d028 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -17,12 +17,15 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.domain.NetworkInfo +import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* +import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal /** * State builder creates a specific states for SwapScreen @@ -51,7 +54,7 @@ internal class StateBuilder( amountTextFieldValue = null, tokenIconUrl = initialCurrency.iconUrl, tokenCurrency = initialCurrency.symbol, - coinId = null, + coinId = initialCurrency.network.backendId, canSelectAnotherToken = false, isNotNativeToken = initialCurrency is CryptoCurrency.Token, balance = "", @@ -79,6 +82,7 @@ internal class StateBuilder( updateInProgress = true, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, onCancelPermissionBottomSheet = actions.hidePermissionBottomSheet, + providerState = ProviderState.Loading(), ) } @@ -146,7 +150,7 @@ internal class StateBuilder( amountEquivalent = null, tokenIconUrl = fromToken.iconUrl, tokenCurrency = fromToken.symbol, - coinId = fromToken.id.value, + coinId = fromToken.network.backendId, isNotNativeToken = fromToken is CryptoCurrency.Token, canSelectAnotherToken = canSelectSendToken, balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "", @@ -158,7 +162,7 @@ internal class StateBuilder( amountEquivalent = null, tokenIconUrl = toToken.iconUrl, tokenCurrency = toToken.symbol, - coinId = toToken.id.value, + coinId = toToken.network.backendId, isNotNativeToken = toToken is CryptoCurrency.Token, canSelectAnotherToken = canSelectReceiveToken, balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "", @@ -185,6 +189,7 @@ internal class StateBuilder( uiStateHolder: SwapStateHolder, quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, + swapProvider: SwapProvider, onFeeSetup: (TxFee) -> Unit, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -213,29 +218,31 @@ internal class StateBuilder( ) } val feeState = createFeeState(quoteModel, uiStateHolder, onFeeSetup) + val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus + val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, - amountEquivalent = quoteModel.fromTokenInfo.tokenFiatBalance, + amountEquivalent = fromCurrencyStatus.getFormattedAmount(quoteModel.fromTokenInfo.amountFiat), tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - coinId = quoteModel.fromTokenInfo.coinId, + coinId = fromCurrencyStatus.currency.network.backendId, isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, - balance = quoteModel.fromTokenInfo.tokenWalletBalance, + balance = fromCurrencyStatus.getFormattedAmount(), isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardState.SwapCardData( type = TransactionCardType.ReceiveCard(), amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()), - amountEquivalent = quoteModel.toTokenInfo.tokenFiatBalance, + amountEquivalent = toCurrencyStatus.getFormattedAmount(quoteModel.toTokenInfo.amountFiat), tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, - coinId = quoteModel.toTokenInfo.coinId, + coinId = toCurrencyStatus.currency.network.backendId, isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, - balance = quoteModel.toTokenInfo.tokenWalletBalance, + balance = toCurrencyStatus.getFormattedAmount(), isBalanceHidden = isBalanceHiddenProvider(), ), networkCurrency = quoteModel.networkCurrency, @@ -256,6 +263,10 @@ internal class StateBuilder( onClick = actions.onSwapClick, ), updateInProgress = false, + providerState = swapProvider.convertToContentClickableProviderState( + fromTokenInfo = quoteModel.fromTokenInfo, + toTokenInfo = quoteModel.toTokenInfo, + ), ) } @@ -298,6 +309,7 @@ internal class StateBuilder( onClick = { }, ), updateInProgress = false, + providerState = ProviderState.Empty(), ) } @@ -726,12 +738,37 @@ internal class StateBuilder( return "$firstAddressPart...$secondAddressPart" } + private fun SwapProvider.convertToContentClickableProviderState( + fromTokenInfo: TokenSwapInfo, + toTokenInfo: TokenSwapInfo, + ): ProviderState { + val rate = fromTokenInfo.tokenAmount.value / toTokenInfo.tokenAmount.value + val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol + val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol + val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" + return ProviderState.Content( + id = this.providerId, + name = this.name ?: "", + iconUrl = this.imageLarge ?: "", + type = this.type.toString(), + rate = rateString, + additionalBadge = ProviderState.AdditionalBadge.BestTrade, + selectionType = ProviderState.SelectionType.CLICK, + percentLowerThenBest = null, + onProviderClick = {}, + ) + } + private fun CryptoCurrencyStatus.getFormattedAmount(): String { val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) } + private fun CryptoCurrencyStatus.getFormattedAmount(amount: BigDecimal): String { + return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) + } + @Suppress("UnusedPrivateMember") private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 153e3cd1b4..3e87fc6ed8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -32,6 +32,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.domain.models.ui.TxFee import com.tangem.feature.swap.models.* +import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @@ -77,6 +78,8 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick: ) { MainInfo(state) + ProviderItemBlock(state = state.providerState) + FeeItem(feeState = state.fee, currency = state.networkCurrency) if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings) @@ -476,6 +479,7 @@ private val state = SwapStateHolder( onChangeCardsClicked = {}, permissionState = SwapPermissionState.InProgress, blockchainId = "POLYGON", + providerState = ProviderState.Loading(), ) @Preview diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index 9c756453d6..a1da108e5f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -2,10 +2,8 @@ package com.tangem.feature.swap.viewmodels import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.Currency -import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData -import com.tangem.feature.swap.domain.models.ui.SwapStateData -import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress -import com.tangem.feature.swap.domain.models.ui.TxFee +import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.feature.swap.domain.models.ui.* data class SwapProcessDataState( // Initial network id @@ -22,4 +20,6 @@ data class SwapProcessDataState( val swapDataModel: SwapStateData? = null, val selectedFee: TxFee? = null, val tokensDataState: TokensDataStateExpress? = null, + val selectedProvider: SwapProvider? = null, + val lastLoadedSwapStates: Map = emptyMap(), ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 03199e51f5..cef1abb918 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -18,7 +18,6 @@ import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.domain.PermissionOptions -import com.tangem.feature.swap.domain.models.domain.RateType import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* @@ -74,7 +73,7 @@ internal class SwapViewModel @Inject constructor( private val inputNumberFormatter = InputNumberFormatter(NumberFormat.getInstance(Locale.getDefault()) as DecimalFormat) private val amountDebouncer = Debouncer() - private val singleTaskScheduler = SingleTaskScheduler() + private val singleTaskScheduler = SingleTaskScheduler>() private var dataState by mutableStateOf(SwapProcessDataState(networkId = initialCryptoCurrency.network.backendId)) @@ -174,10 +173,10 @@ internal class SwapViewModel @Inject constructor( ) } else { startLoadingQuotes( - fromToken = fromCurrencyStatus.currency, - toToken = selectedCurrency.currency, + fromToken = fromCurrencyStatus, + toToken = selectedCurrency, amount = lastAmount.value, - toProvidersList = findSwapProviders(fromCurrencyStatus, selectedCurrency) + toProvidersList = findSwapProviders(fromCurrencyStatus, selectedCurrency), ) } } @@ -196,13 +195,18 @@ internal class SwapViewModel @Inject constructor( } private fun startLoadingQuotes( - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, amount: String, toProvidersList: List, ) { singleTaskScheduler.cancelTask() - uiState = stateBuilder.createQuotesLoadingState(uiState, fromToken, toToken, initialCryptoCurrency.id.value) + uiState = stateBuilder.createQuotesLoadingState( + uiState, + fromToken.currency, + toToken.currency, + initialCryptoCurrency.id.value, + ) singleTaskScheduler.scheduleTask( viewModelScope, loadQuotesTask( @@ -220,20 +224,20 @@ internal class SwapViewModel @Inject constructor( val amount = dataState.amount if (fromCurrency != null && toCurrency != null && amount != null) { startLoadingQuotes( - fromToken = fromCurrency.currency, - toToken = toCurrency.currency, + fromToken = fromCurrency, + toToken = toCurrency, amount = amount, - toProvidersList = findSwapProviders(fromCurrency, toCurrency) + toProvidersList = findSwapProviders(fromCurrency, toCurrency), ) } } private fun loadQuotesTask( - fromToken: CryptoCurrency, - toToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, amount: String, toProvidersList: List, - ): PeriodicTask { + ): PeriodicTask> { return PeriodicTask( UPDATE_DELAY, task = { @@ -251,17 +255,19 @@ internal class SwapViewModel @Inject constructor( providers = toProvidersList, amountToSwap = amount, selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, - ).entries.first().value// TODO + ) } }, - onSuccess = { swapState -> - when (swapState) { + onSuccess = { providersState -> + val (provider, state) = updateLoadedQuotes(providersState) + when (state) { is SwapState.QuotesLoadedState -> { - fillDataState(swapState.permissionState, swapState.swapDataModel) + fillDataState(state.permissionState, state.swapDataModel) uiState = stateBuilder.createQuotesLoadedState( uiStateHolder = uiState, - quoteModel = swapState, - fromToken = fromToken, + quoteModel = state, + fromToken = fromToken.currency, + swapProvider = provider, ) { updatedFee -> dataState = dataState.copy( selectedFee = updatedFee, @@ -271,12 +277,12 @@ internal class SwapViewModel @Inject constructor( is SwapState.EmptyAmountState -> { uiState = stateBuilder.createQuotesEmptyAmountState( uiStateHolder = uiState, - emptyAmountState = swapState, + emptyAmountState = state, ) } is SwapState.SwapError -> { - Timber.e("SwapError when loading quotes ${swapState.error}") - uiState = stateBuilder.mapError(uiState, swapState.error) { startLoadingQuotesFromLastState() } + Timber.e("SwapError when loading quotes ${state.error}") + uiState = stateBuilder.mapError(uiState, state.error) { startLoadingQuotesFromLastState() } } } }, @@ -287,6 +293,15 @@ internal class SwapViewModel @Inject constructor( ) } + private fun updateLoadedQuotes(state: Map): Pair { + val selectedSwapProvider = dataState.selectedProvider ?: state.keys.first() + dataState = dataState.copy( + selectedProvider = selectedSwapProvider, + lastLoadedSwapStates = state, + ) + return state.entries.first { it.key == selectedSwapProvider }.toPair() + } + private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapStateData?) { dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) { dataState.copy( @@ -435,10 +450,10 @@ internal class SwapViewModel @Inject constructor( toCryptoCurrency = toToken, ) startLoadingQuotes( - fromToken = fromToken.currency, - toToken = toToken.currency, + fromToken = fromToken, + toToken = toToken, amount = lastAmount.value, - toProvidersList = findSwapProviders(fromToken, toToken) + toProvidersList = findSwapProviders(fromToken, toToken), ) swapRouter.openScreen(SwapNavScreen.Main) } @@ -460,10 +475,10 @@ internal class SwapViewModel @Inject constructor( inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), ) startLoadingQuotes( - fromToken = newFromToken.currency, - toToken = newToToken.currency, + fromToken = newFromToken, + toToken = newToToken, amount = lastAmount.value, - toProvidersList = findSwapProviders(newFromToken, newToToken) + toProvidersList = findSwapProviders(newFromToken, newToToken), ) } } @@ -479,10 +494,10 @@ internal class SwapViewModel @Inject constructor( stateBuilder.updateSwapAmount(uiState, inputNumberFormatter.formatWithThousands(cutValue, decimals)) amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { startLoadingQuotes( - fromToken = fromToken.currency, - toToken = toToken.currency, + fromToken = fromToken, + toToken = toToken, amount = lastAmount.value, - toProvidersList = findSwapProviders(fromToken, toToken) + toProvidersList = findSwapProviders(fromToken, toToken), ) } } From 0ff8622215f44d89e71b7ec5d5c92a1a3d8b5636 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Nov 2023 22:24:38 +0300 Subject: [PATCH 039/139] Updated on 2026-08-14 --- .../tangem/feature/swap/SwapRepositoryImpl.kt | 2 - .../feature/swap/domain/SwapInteractorImpl.kt | 23 - features/swap/presentation/build.gradle.kts | 1 + .../feature/swap/models/SwapStateHolder.kt | 2 + .../tangem/feature/swap/models/UiActions.kt | 2 + .../states/GivePermissionBottomSheetConfig.kt | 9 + .../swap/ui/ChooseProviderBottomSheet.kt | 2 +- .../tangem/feature/swap/ui/ProviderItem.kt | 2 +- .../tangem/feature/swap/ui/StateBuilder.kt | 90 ++- .../swap/ui/SwapPermissionBottomSheet.kt | 327 ++++++++++ .../ui/SwapPermissionBottomSheetContent.kt | 592 +++++++++--------- .../com/tangem/feature/swap/ui/SwapScreen.kt | 113 +--- .../feature/swap/ui/SwapScreenContent.kt | 20 +- .../feature/swap/viewmodels/SwapViewModel.kt | 95 ++- 14 files changed, 813 insertions(+), 467 deletions(-) create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 00d5f2448a..f63f42e5f3 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -43,11 +43,9 @@ internal class SwapRepositoryImpl @Inject constructor( ) : SwapRepository { private val tokensConverter = TokensConverter() - private val quotesConverter = QuotesConverter() private val swapConverter = SwapConverter() private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() - private val rateTypeConverter = RateTypeConverter() private val swapProviderConverter = SwapProviderConverter() override suspend fun getPairs( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 1dd4dab66c..28de49180c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -389,27 +389,6 @@ internal class SwapInteractorImpl @Inject constructor( } } - @Deprecated("used in old swap mechanism") - private fun selectToToken( - initialToken: Currency, - tokensInWallet: List, - loadedTokens: List, - ): Currency { - val toToken = if (tokensInWallet.isNotEmpty()) { - tokensInWallet.firstOrNull { it.symbol != initialToken.symbol } - ?: loadedTokens.first { it.symbol != initialToken.symbol } - } else { - val findUsdt = loadedTokens.firstOrNull { it.symbol == USDT_SYMBOL && it.symbol != initialToken.symbol } - if (findUsdt == null) { - val findUsdc = loadedTokens.firstOrNull { it.symbol == USDC_SYMBOL && it.symbol != initialToken.symbol } - findUsdc ?: loadedTokens.first { it.symbol != initialToken.symbol } - } else { - findUsdt - } - } - return toToken - } - @Suppress("UnusedPrivateMember") @Deprecated("used in old swap mechanism") private fun getTokensWithBalance( @@ -890,8 +869,6 @@ internal class SwapInteractorImpl @Inject constructor( @Suppress("UnusedPrivateMember") private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.0 // if need to increase fee when check isEnough private const val INCREASE_GAS_LIMIT_BY = 112 // 12% - private const val USDT_SYMBOL = "USDT" - private const val USDC_SYMBOL = "USDC" private const val INFINITY_SYMBOL = "∞" private val ONE_INCH_SUPPORTED_NETWORKS = listOf( diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 86879ce609..dc0cdcabd2 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation(deps.arrow.core) implementation(deps.compose.foundation) implementation(deps.compose.material) + implementation(deps.compose.material3) implementation(deps.compose.ui.tooling) implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index e166729104..82b62f10c7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.models import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState @@ -22,6 +23,7 @@ data class SwapStateHolder( val permissionState: SwapPermissionState = SwapPermissionState.Empty, val successState: SwapSuccessStateHolder? = null, val selectTokenState: SwapSelectTokenStateHolder? = null, + val bottomSheetConfig: TangemBottomSheetConfig? = null, val swapButton: SwapButton, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 0dc67646d0..7beb38ac1e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -21,4 +21,6 @@ data class UiActions( // region new actions val onClickFee: () -> Unit, val onSelectFeeType: (FeeType) -> Unit, + val onProviderClick: (String) -> Unit, + val onProviderSelect: (String) -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt new file mode 100644 index 0000000000..57628ded51 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.feature.swap.models.states + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.feature.swap.models.SwapPermissionState + +class GivePermissionBottomSheetConfig( + val data: SwapPermissionState.ReadyForRequest, + val onCancel: () -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 266c1f9b49..e93e82c505 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -26,7 +26,7 @@ fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetConfig) { Column( - modifier = Modifier.background(TangemTheme.colors.background.secondary), + modifier = Modifier.background(TangemTheme.colors.background.primary), ) { Text( text = "Choose provider", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index dfe32ead57..1cd636d3e7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -324,7 +324,7 @@ private fun BestTradeItem(modifier: Modifier = Modifier) { ), ) { Text( - text = "Best trade", + text = "Best rate", style = TangemTheme.typography.caption1, color = TangemTheme.colors.icon.accent, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 4cbd99d028..4089832235 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.ui import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.Provider +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState @@ -21,11 +22,14 @@ import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* +import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig +import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal +import java.math.RoundingMode /** * State builder creates a specific states for SwapScreen @@ -224,7 +228,7 @@ internal class StateBuilder( sendCardData = SwapCardState.SwapCardData( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, - amountEquivalent = fromCurrencyStatus.getFormattedAmount(quoteModel.fromTokenInfo.amountFiat), + amountEquivalent = getFormattedFiatAmount(quoteModel.fromTokenInfo.amountFiat), tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, coinId = fromCurrencyStatus.currency.network.backendId, isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, @@ -236,7 +240,7 @@ internal class StateBuilder( receiveCardData = SwapCardState.SwapCardData( type = TransactionCardType.ReceiveCard(), amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()), - amountEquivalent = toCurrencyStatus.getFormattedAmount(quoteModel.toTokenInfo.amountFiat), + amountEquivalent = getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat), tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, coinId = toCurrencyStatus.currency.network.backendId, isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, @@ -266,6 +270,7 @@ internal class StateBuilder( providerState = swapProvider.convertToContentClickableProviderState( fromTokenInfo = quoteModel.fromTokenInfo, toTokenInfo = quoteModel.toTokenInfo, + onProviderClick = actions.onProviderClick, ), ) } @@ -709,6 +714,66 @@ internal class StateBuilder( } } + fun showPermissionBottomSheet(uiState: SwapStateHolder, onDismiss: () -> Unit): SwapStateHolder { + val permissionState = uiState.permissionState + if (permissionState is SwapPermissionState.ReadyForRequest) { + val config = GivePermissionBottomSheetConfig( + data = permissionState, + onCancel = onDismiss, + ) + return uiState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismiss, + content = config, + ), + ) + } + return uiState + } + + fun dismissBottomSheet(uiState: SwapStateHolder): SwapStateHolder { + return uiState.copy( + bottomSheetConfig = uiState.bottomSheetConfig?.copy(isShow = false), + ) + } + + fun showSelectProviderBottomSheet( + uiState: SwapStateHolder, + selectedProviderId: String, + providersStates: Map, + onDismiss: () -> Unit, + ): SwapStateHolder { + val config = ChooseProviderBottomSheetConfig( + selectedProviderId = selectedProviderId, + providers = providersStates.entries + .mapNotNull { it.convertToProviderState(actions.onProviderSelect) } + .toImmutableList(), + ) + return uiState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismiss, + content = config, + ), + ) + } + + private fun Map.Entry.convertToProviderState( + onProviderSelect: (String) -> Unit, + ): ProviderState? { + val provider = this.key + return when (val state = this.value) { + is SwapState.EmptyAmountState -> null + is SwapState.QuotesLoadedState -> provider.convertToContentClickableProviderState( + state.fromTokenInfo, + state.toTokenInfo, + onProviderClick = onProviderSelect, + ) + is SwapState.SwapError -> null + } + } + private fun createPermissionNotificationConfig(fromTokenSymbol: String): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.swapping_permission_header), @@ -724,7 +789,7 @@ internal class StateBuilder( return NotificationConfig( title = resourceReference(R.string.swapping_high_price_impact), subtitle = resourceReference(R.string.swapping_high_price_impact_description), - iconResId = R.drawable.ic_locked_24, + iconResId = R.drawable.ic_alert_circle_24, ) } @@ -741,8 +806,13 @@ internal class StateBuilder( private fun SwapProvider.convertToContentClickableProviderState( fromTokenInfo: TokenSwapInfo, toTokenInfo: TokenSwapInfo, + onProviderClick: (String) -> Unit, ): ProviderState { - val rate = fromTokenInfo.tokenAmount.value / toTokenInfo.tokenAmount.value + val rate = toTokenInfo.tokenAmount.value.divide( + fromTokenInfo.tokenAmount.value, + toTokenInfo.cryptoCurrencyStatus.currency.decimals, + RoundingMode.HALF_UP, + ) val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" @@ -755,7 +825,7 @@ internal class StateBuilder( additionalBadge = ProviderState.AdditionalBadge.BestTrade, selectionType = ProviderState.SelectionType.CLICK, percentLowerThenBest = null, - onProviderClick = {}, + onProviderClick = onProviderClick, ) } @@ -765,10 +835,6 @@ internal class StateBuilder( return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) } - private fun CryptoCurrencyStatus.getFormattedAmount(amount: BigDecimal): String { - return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) - } - @Suppress("UnusedPrivateMember") private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN @@ -777,6 +843,12 @@ internal class StateBuilder( return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) } + private fun getFormattedFiatAmount(amount: BigDecimal): String { + val appCurrency = appCurrencyProvider() + + return BigDecimalFormatter.formatFiatAmount(amount, appCurrency.code, appCurrency.symbol) + } + private companion object { const val ADDRESS_MIN_LENGTH = 11 const val ADDRESS_FIRST_PART_LENGTH = 7 diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt new file mode 100644 index 0000000000..724c3e44d8 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt @@ -0,0 +1,327 @@ +package com.tangem.feature.swap.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.atoms.Hand +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.models.ApprovePermissionButton +import com.tangem.feature.swap.models.ApproveType +import com.tangem.feature.swap.models.CancelPermissionButton +import com.tangem.feature.swap.models.SwapPermissionState +import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig +import com.tangem.feature.swap.presentation.R +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun SwapPermissionBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet(config) { content: GivePermissionBottomSheetConfig -> + SwapPermissionBottomSheetContent(content = content) + } +} + +@Composable +private fun SwapPermissionBottomSheetContent(content: GivePermissionBottomSheetConfig) { + var isPermissionAlertShow by remember { mutableStateOf(false) } + val data = content.data + Column( + modifier = Modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Hand() + + SpacerH10() + + Box(modifier = Modifier.fillMaxWidth()) { + Text( + modifier = Modifier.align(Alignment.Center), + text = stringResource(id = R.string.swapping_permission_header), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + ) + IconButton( + modifier = Modifier.align(Alignment.CenterEnd), + onClick = { isPermissionAlertShow = true }, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_question_24), + contentDescription = null, + ) + } + } + + SpacerH10() + + Text( + text = stringResource( + id = R.string.swapping_permission_subheader, + data.currency, + ), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing8), + ) + + SpacerH16() + + ApprovalBottomSheetInfo(data) + + SpacerH28() + + PrimaryButtonIconEnd( + text = stringResource(id = R.string.swapping_permission_buttons_approve), + iconResId = R.drawable.ic_tangem_24, + modifier = Modifier.fillMaxWidth(), + onClick = data.approveButton.onClick, + ) + + SpacerH12() + + SecondaryButton( + text = stringResource(id = R.string.common_cancel), + modifier = Modifier.fillMaxWidth(), + onClick = { + content.onCancel() + }, + ) + + SpacerH16() + + // region dialog + if (isPermissionAlertShow) { + BasicDialog( + message = stringResource(id = R.string.swapping_approve_information_text), + title = stringResource(id = R.string.swapping_approve_information_title), + confirmButton = DialogButton { isPermissionAlertShow = false }, + onDismissDialog = {}, + ) + } + } +} + +@Composable +private fun ApprovalBottomSheetInfo(data: SwapPermissionState.ReadyForRequest) { + Column( + modifier = Modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AmountItem( + currency = data.currency, + approveType = data.approveType, + onChangeApproveType = data.onChangeApproveType, + approveItems = data.approveItems, + ) + SubtitleItem( + subtitle = stringResource(id = R.string.swapping_permission_policy_type_footer), + modifier = Modifier.fillMaxWidth(), + ) + SpacerH24() + DividerBottomSheet() + FeeItem(fee = data.fee.resolveReference()) + SubtitleItem( + subtitle = stringResource(id = R.string.swapping_permission_fee_footer), + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun DividerBottomSheet() { + Divider( + color = TangemTheme.colors.stroke.primary, + thickness = TangemTheme.dimens.size0_5, + ) +} + +@Composable +private fun InformationItem(subtitle: String, value: String) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing16), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = subtitle, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + + MiddleEllipsisText( + text = value, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), + ) + } +} + +@Composable +private fun AmountItem( + currency: String, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, +) { + var isExpandSelector by remember { + mutableStateOf(false) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing16), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.swapping_permission_rows_amount, currency), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + Box { + SelectorItem( + getTitleForApproveType(approveType = approveType), + ) { + isExpandSelector = true + } + DropdownSelector( + isExpanded = isExpandSelector, + onDismiss = { isExpandSelector = false }, + onItemClick = { approveType -> + isExpandSelector = false + onChangeApproveType.invoke(approveType) + }, + items = approveItems, + ) + } + } +} + +@Composable +private fun SelectorItem(title: String, onClick: () -> Unit) { + Row( + modifier = Modifier.clickable { onClick() }, + ) { + Text( + text = title, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + Icon( + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } +} + +@Composable +private fun DropdownSelector( + isExpanded: Boolean, + onDismiss: () -> Unit, + onItemClick: (ApproveType) -> Unit, + items: ImmutableList, +) { + DropdownMenu( + expanded = isExpanded, + onDismissRequest = onDismiss, + modifier = Modifier + .wrapContentSize() + .background(TangemTheme.colors.background.secondary), + ) { + items.forEach { item -> + DropdownMenuItem( + onClick = { + onItemClick.invoke(item) + }, + ) { + Text( + text = getTitleForApproveType(approveType = item), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + } + } + } +} + +@Composable +private fun FeeItem(fee: String) { + InformationItem( + subtitle = stringResource(id = R.string.send_fee_label), + value = fee, + ) +} + +@Composable +private fun SubtitleItem(subtitle: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = subtitle, + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + ) +} + +@Composable +private fun getTitleForApproveType(approveType: ApproveType): String = when (approveType) { + ApproveType.LIMITED -> stringResource(id = R.string.swapping_permission_current_transaction) + ApproveType.UNLIMITED -> stringResource(id = R.string.swapping_permission_unlimited) +} + +// region preview + +@Preview +@Composable +private fun Preview_AgreementBottomSheet_InLightTheme() { + TangemTheme(isDark = false) { + SwapPermissionBottomSheetContent(content = previewData) + } +} + +@Preview +@Composable +private fun Preview_AgreementBottomSheet_InDarkTheme() { + TangemTheme(isDark = true) { + SwapPermissionBottomSheetContent(content = previewData) + } +} + +private val previewData = GivePermissionBottomSheetConfig( + data = SwapPermissionState.ReadyForRequest( + currency = "DAI", + amount = "∞", + walletAddress = "", + spenderAddress = "", + fee = TextReference.Str("2,14$"), + approveType = ApproveType.UNLIMITED, + approveButton = ApprovePermissionButton(true) {}, + cancelButton = CancelPermissionButton(true), + onChangeApproveType = { ApproveType.UNLIMITED }, + ), + onCancel = {}, +) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt index 6f8bee6fe7..617534b273 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt @@ -1,315 +1,297 @@ package com.tangem.feature.swap.ui -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.atoms.Hand -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.swap.models.ApprovePermissionButton -import com.tangem.feature.swap.models.ApproveType -import com.tangem.feature.swap.models.CancelPermissionButton -import com.tangem.feature.swap.models.SwapPermissionState -import com.tangem.feature.swap.presentation.R -import kotlinx.collections.immutable.ImmutableList -@Composable -fun SwapPermissionBottomSheetContent(data: SwapPermissionState.ReadyForRequest, onCancel: () -> Unit) { - var isPermissionAlertShow by remember { mutableStateOf(false) } - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing16), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Hand() +// @Composable +// fun SwapPermissionBottomSheetContent(data: SwapPermissionState.ReadyForRequest, onCancel: () -> Unit) { +// var isPermissionAlertShow by remember { mutableStateOf(false) } +// Column( +// modifier = Modifier +// .background(color = TangemTheme.colors.background.primary) +// .fillMaxWidth() +// .padding(horizontal = TangemTheme.dimens.spacing16), +// horizontalAlignment = Alignment.CenterHorizontally, +// ) { +// Hand() +// +// SpacerH10() +// +// Box(modifier = Modifier.fillMaxWidth()) { +// Text( +// modifier = Modifier.align(Alignment.Center), +// text = stringResource(id = R.string.swapping_permission_header), +// color = TangemTheme.colors.text.primary1, +// style = TangemTheme.typography.subtitle1, +// ) +// IconButton( +// modifier = Modifier.align(Alignment.CenterEnd), +// onClick = { isPermissionAlertShow = true }, +// ) { +// Icon( +// painter = painterResource(id = R.drawable.ic_question_24), +// contentDescription = null, +// ) +// } +// } +// +// SpacerH10() +// +// Text( +// text = stringResource( +// id = R.string.swapping_permission_subheader, +// data.currency, +// ), +// color = TangemTheme.colors.text.secondary, +// style = TangemTheme.typography.body2, +// textAlign = TextAlign.Center, +// modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing8), +// ) +// +// SpacerH16() +// +// ApprovalBottomSheetInfo(data) +// +// SpacerH28() +// +// PrimaryButtonIconEnd( +// text = stringResource(id = R.string.swapping_permission_buttons_approve), +// iconResId = R.drawable.ic_tangem_24, +// modifier = Modifier.fillMaxWidth(), +// onClick = data.approveButton.onClick, +// ) +// +// SpacerH12() +// +// SecondaryButton( +// text = stringResource(id = R.string.common_cancel), +// modifier = Modifier.fillMaxWidth(), +// onClick = { +// onCancel() +// }, +// ) +// +// SpacerH16() +// +// // region dialog +// if (isPermissionAlertShow) { +// BasicDialog( +// message = stringResource(id = R.string.swapping_approve_information_text), +// title = stringResource(id = R.string.swapping_approve_information_title), +// confirmButton = DialogButton { isPermissionAlertShow = false }, +// onDismissDialog = {}, +// ) +// } +// } +// } - SpacerH10() - - Box(modifier = Modifier.fillMaxWidth()) { - Text( - modifier = Modifier.align(Alignment.Center), - text = stringResource(id = R.string.swapping_permission_header), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - ) - IconButton( - modifier = Modifier.align(Alignment.CenterEnd), - onClick = { isPermissionAlertShow = true }, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_question_24), - contentDescription = null, - ) - } - } - - SpacerH10() - - Text( - text = stringResource( - id = R.string.swapping_permission_subheader, - data.currency, - ), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing8), - ) - - SpacerH16() - - ApprovalBottomSheetInfo(data) - - SpacerH28() - - PrimaryButtonIconEnd( - text = stringResource(id = R.string.swapping_permission_buttons_approve), - iconResId = R.drawable.ic_tangem_24, - modifier = Modifier.fillMaxWidth(), - onClick = data.approveButton.onClick, - ) - - SpacerH12() - - SecondaryButton( - text = stringResource(id = R.string.common_cancel), - modifier = Modifier.fillMaxWidth(), - onClick = { - onCancel() - }, - ) - - SpacerH16() - - // region dialog - if (isPermissionAlertShow) { - BasicDialog( - message = stringResource(id = R.string.swapping_approve_information_text), - title = stringResource(id = R.string.swapping_approve_information_title), - confirmButton = DialogButton { isPermissionAlertShow = false }, - onDismissDialog = {}, - ) - } - } -} - -@Composable -private fun ApprovalBottomSheetInfo(data: SwapPermissionState.ReadyForRequest) { - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AmountItem( - currency = data.currency, - approveType = data.approveType, - onChangeApproveType = data.onChangeApproveType, - approveItems = data.approveItems, - ) - SubtitleItem( - subtitle = stringResource(id = R.string.swapping_permission_policy_type_footer), - modifier = Modifier.fillMaxWidth(), - ) - SpacerH24() - DividerBottomSheet() - FeeItem(fee = data.fee.resolveReference()) - SubtitleItem( - subtitle = stringResource(id = R.string.swapping_permission_fee_footer), - modifier = Modifier.fillMaxWidth(), - ) - } -} - -@Composable -private fun DividerBottomSheet() { - Divider( - color = TangemTheme.colors.stroke.primary, - thickness = TangemTheme.dimens.size0_5, - ) -} - -@Composable -private fun InformationItem(subtitle: String, value: String) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing16), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = subtitle, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - maxLines = 1, - ) - - MiddleEllipsisText( - text = value, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), - ) - } -} - -@Composable -private fun AmountItem( - currency: String, - approveType: ApproveType, - approveItems: ImmutableList, - onChangeApproveType: (ApproveType) -> Unit, -) { - var isExpandSelector by remember { - mutableStateOf(false) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing16), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(id = R.string.swapping_permission_rows_amount, currency), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - maxLines = 1, - ) - Box { - SelectorItem( - getTitleForApproveType(approveType = approveType), - ) { - isExpandSelector = true - } - DropdownSelector( - isExpanded = isExpandSelector, - onDismiss = { isExpandSelector = false }, - onItemClick = { approveType -> - isExpandSelector = false - onChangeApproveType.invoke(approveType) - }, - items = approveItems, - ) - } - } -} - -@Composable -private fun SelectorItem(title: String, onClick: () -> Unit) { - Row( - modifier = Modifier.clickable { onClick() }, - ) { - Text( - text = title, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body1, - maxLines = 1, - ) - Icon( - painter = painterResource(id = R.drawable.ic_chevron_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } -} - -@Composable -private fun DropdownSelector( - isExpanded: Boolean, - onDismiss: () -> Unit, - onItemClick: (ApproveType) -> Unit, - items: ImmutableList, -) { - DropdownMenu( - expanded = isExpanded, - onDismissRequest = onDismiss, - modifier = Modifier - .wrapContentSize() - .background(TangemTheme.colors.background.secondary), - ) { - items.forEach { item -> - DropdownMenuItem( - onClick = { - onItemClick.invoke(item) - }, - ) { - Text( - text = getTitleForApproveType(approveType = item), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body1, - maxLines = 1, - ) - } - } - } -} - -@Composable -private fun FeeItem(fee: String) { - InformationItem( - subtitle = stringResource(id = R.string.send_fee_label), - value = fee, - ) -} - -@Composable -private fun SubtitleItem(subtitle: String, modifier: Modifier = Modifier) { - Text( - modifier = modifier, - text = subtitle, - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - ) -} - -@Composable -private fun getTitleForApproveType(approveType: ApproveType): String = when (approveType) { - ApproveType.LIMITED -> stringResource(id = R.string.swapping_permission_current_transaction) - ApproveType.UNLIMITED -> stringResource(id = R.string.swapping_permission_unlimited) -} - -// region preview - -@Preview -@Composable -private fun Preview_AgreementBottomSheet_InLightTheme() { - TangemTheme(isDark = false) { - SwapPermissionBottomSheetContent(data = previewData) {} - } -} - -@Preview -@Composable -private fun Preview_AgreementBottomSheet_InDarkTheme() { - TangemTheme(isDark = true) { - SwapPermissionBottomSheetContent(data = previewData) {} - } -} - -private val previewData = SwapPermissionState.ReadyForRequest( - currency = "DAI", - amount = "∞", - walletAddress = "", - spenderAddress = "", - fee = TextReference.Str("2,14$"), - approveType = ApproveType.UNLIMITED, - approveButton = ApprovePermissionButton(true) {}, - cancelButton = CancelPermissionButton(true), - onChangeApproveType = { ApproveType.UNLIMITED }, -) +// @Composable +// private fun ApprovalBottomSheetInfo(data: SwapPermissionState.ReadyForRequest) { +// Column( +// modifier = Modifier +// .background(color = TangemTheme.colors.background.primary) +// .fillMaxWidth(), +// horizontalAlignment = Alignment.CenterHorizontally, +// ) { +// AmountItem( +// currency = data.currency, +// approveType = data.approveType, +// onChangeApproveType = data.onChangeApproveType, +// approveItems = data.approveItems, +// ) +// SubtitleItem( +// subtitle = stringResource(id = R.string.swapping_permission_policy_type_footer), +// modifier = Modifier.fillMaxWidth(), +// ) +// SpacerH24() +// DividerBottomSheet() +// FeeItem(fee = data.fee.resolveReference()) +// SubtitleItem( +// subtitle = stringResource(id = R.string.swapping_permission_fee_footer), +// modifier = Modifier.fillMaxWidth(), +// ) +// } +// } +// +// @Composable +// private fun DividerBottomSheet() { +// Divider( +// color = TangemTheme.colors.stroke.primary, +// thickness = TangemTheme.dimens.size0_5, +// ) +// } +// +// @Composable +// private fun InformationItem(subtitle: String, value: String) { +// Row( +// modifier = Modifier +// .fillMaxWidth() +// .padding(vertical = TangemTheme.dimens.spacing16), +// horizontalArrangement = Arrangement.SpaceBetween, +// verticalAlignment = Alignment.CenterVertically, +// ) { +// Text( +// text = subtitle, +// color = TangemTheme.colors.text.primary1, +// style = TangemTheme.typography.subtitle1, +// maxLines = 1, +// ) +// +// MiddleEllipsisText( +// text = value, +// color = TangemTheme.colors.text.tertiary, +// style = TangemTheme.typography.body2, +// modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), +// ) +// } +// } +// +// @Composable +// private fun AmountItem( +// currency: String, +// approveType: ApproveType, +// approveItems: ImmutableList, +// onChangeApproveType: (ApproveType) -> Unit, +// ) { +// var isExpandSelector by remember { +// mutableStateOf(false) +// } +// Row( +// modifier = Modifier +// .fillMaxWidth() +// .padding(vertical = TangemTheme.dimens.spacing16), +// horizontalArrangement = Arrangement.SpaceBetween, +// verticalAlignment = Alignment.CenterVertically, +// ) { +// Text( +// text = stringResource(id = R.string.swapping_permission_rows_amount, currency), +// color = TangemTheme.colors.text.primary1, +// style = TangemTheme.typography.subtitle1, +// maxLines = 1, +// ) +// Box { +// SelectorItem( +// getTitleForApproveType(approveType = approveType), +// ) { +// isExpandSelector = true +// } +// DropdownSelector( +// isExpanded = isExpandSelector, +// onDismiss = { isExpandSelector = false }, +// onItemClick = { approveType -> +// isExpandSelector = false +// onChangeApproveType.invoke(approveType) +// }, +// items = approveItems, +// ) +// } +// } +// } +// +// @Composable +// private fun SelectorItem(title: String, onClick: () -> Unit) { +// Row( +// modifier = Modifier.clickable { onClick() }, +// ) { +// Text( +// text = title, +// color = TangemTheme.colors.text.primary1, +// style = TangemTheme.typography.body1, +// maxLines = 1, +// ) +// Icon( +// painter = painterResource(id = R.drawable.ic_chevron_24), +// tint = TangemTheme.colors.icon.primary1, +// contentDescription = null, +// ) +// } +// } +// +// @Composable +// private fun DropdownSelector( +// isExpanded: Boolean, +// onDismiss: () -> Unit, +// onItemClick: (ApproveType) -> Unit, +// items: ImmutableList, +// ) { +// DropdownMenu( +// expanded = isExpanded, +// onDismissRequest = onDismiss, +// modifier = Modifier +// .wrapContentSize() +// .background(TangemTheme.colors.background.secondary), +// ) { +// items.forEach { item -> +// DropdownMenuItem( +// onClick = { +// onItemClick.invoke(item) +// }, +// ) { +// Text( +// text = getTitleForApproveType(approveType = item), +// color = TangemTheme.colors.text.primary1, +// style = TangemTheme.typography.body1, +// maxLines = 1, +// ) +// } +// } +// } +// } +// +// @Composable +// private fun FeeItem(fee: String) { +// InformationItem( +// subtitle = stringResource(id = R.string.send_fee_label), +// value = fee, +// ) +// } +// +// @Composable +// private fun SubtitleItem(subtitle: String, modifier: Modifier = Modifier) { +// Text( +// modifier = modifier, +// text = subtitle, +// color = TangemTheme.colors.text.secondary, +// style = TangemTheme.typography.body2, +// ) +// } +// +// @Composable +// private fun getTitleForApproveType(approveType: ApproveType): String = when (approveType) { +// ApproveType.LIMITED -> stringResource(id = R.string.swapping_permission_current_transaction) +// ApproveType.UNLIMITED -> stringResource(id = R.string.swapping_permission_unlimited) +// } +// +// // region preview +// +// @Preview +// @Composable +// private fun Preview_AgreementBottomSheet_InLightTheme() { +// TangemTheme(isDark = false) { +// SwapPermissionBottomSheetContent(data = previewData) {} +// } +// } +// +// @Preview +// @Composable +// private fun Preview_AgreementBottomSheet_InDarkTheme() { +// TangemTheme(isDark = true) { +// SwapPermissionBottomSheetContent(data = previewData) {} +// } +// } +// +// private val previewData = SwapPermissionState.ReadyForRequest( +// currency = "DAI", +// amount = "∞", +// walletAddress = "", +// spenderAddress = "", +// fee = TextReference.Str("2,14$"), +// approveType = ApproveType.UNLIMITED, +// approveButton = ApprovePermissionButton(true) {}, +// cancelButton = CancelPermissionButton(true), +// onChangeApproveType = { ApproveType.UNLIMITED }, +// ) //endregion preview \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 850d4434df..692d6a4e57 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -1,99 +1,44 @@ package com.tangem.feature.swap.ui import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.padding import androidx.compose.material.* +import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.swap.models.SwapPermissionState import com.tangem.feature.swap.models.SwapStateHolder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig +import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig +import com.tangem.feature.swap.presentation.R -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalLayoutApi::class) @Composable internal fun SwapScreen(stateHolder: SwapStateHolder) { - val coroutineScope = rememberCoroutineScope() - val bottomSheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden, - skipHalfExpanded = true, - ) - BackHandler( - onBack = { - if (bottomSheetState.isVisible) { - hideBottomSheet(coroutineScope, stateHolder, bottomSheetState) - } else { - stateHolder.onBackClicked() - } - }, - ) + BackHandler(onBack = stateHolder.onBackClicked) - LaunchedEffect(bottomSheetState.targetValue) { - if (bottomSheetState.targetValue == ModalBottomSheetValue.Hidden) { - stateHolder.onCancelPermissionBottomSheet.invoke() + Scaffold( + containerColor = TangemTheme.colors.background.secondary, + ) { scaffoldPaddings -> + + SwapScreenContent( + state = stateHolder, + modifier = Modifier.padding(scaffoldPaddings), + ) + + stateHolder.bottomSheetConfig?.let { config -> + when (config.content) { + is GivePermissionBottomSheetConfig -> { + SwapPermissionBottomSheet(config = config) + } + is ChooseProviderBottomSheetConfig -> { + ChooseProviderBottomSheet(config = config) + } + } } } - - ModalBottomSheetLayout( - modifier = Modifier.systemBarsPadding(), - sheetContent = { - if (stateHolder.permissionState is SwapPermissionState.ReadyForRequest) { - SwapPermissionBottomSheetContent( - data = stateHolder.permissionState, - onCancel = { - hideBottomSheet(coroutineScope, stateHolder, bottomSheetState) - }, - ) - } else { - // Required "else" block to prevent compose crash - // always close BS if its empty, cause user should not see this - LaunchedEffect(Unit) { - coroutineScope.launch { bottomSheetState.hide() } - } - Box(modifier = Modifier.fillMaxSize()) - } - }, - sheetState = bottomSheetState, - sheetShape = RoundedCornerShape( - topStart = TangemTheme.dimens.radius16, - topEnd = TangemTheme.dimens.radius16, - ), - sheetElevation = TangemTheme.dimens.elevation24, - scrimColor = TangemTheme.colors.background.secondary.copy(alpha = 0.4f), - content = { - SwapScreenContent( - state = stateHolder, - onPermissionWarningClick = { - val isBottomSheetReady = !bottomSheetState.isVisible && - stateHolder.permissionState is SwapPermissionState.ReadyForRequest - coroutineScope.launch { - if (isBottomSheetReady) { - bottomSheetState.show() - stateHolder.onShowPermissionBottomSheet.invoke() - } else { - bottomSheetState.hide() - } - } - }, - ) - }, - ) -} - -@OptIn(ExperimentalMaterialApi::class) -private fun hideBottomSheet( - coroutineScope: CoroutineScope, - stateHolder: SwapStateHolder, - bottomSheetState: ModalBottomSheetState, -) { - coroutineScope.launch { - stateHolder.onCancelPermissionBottomSheet.invoke() - bottomSheetState.hide() - } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 3e87fc6ed8..1ad3351901 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue @@ -39,30 +38,21 @@ import java.math.BigDecimal @Suppress("LongMethod") @Composable -internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick: () -> Unit) { +internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modifier) { val keyboard by keyboardAsState() Box( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(color = TangemTheme.colors.background.secondary), ) { - Image( - modifier = Modifier - .size(width = TangemTheme.dimens.size164, height = TangemTheme.dimens.size80) - .align(Alignment.BottomCenter) - .padding(bottom = TangemTheme.dimens.spacing28), - painter = painterResource(id = R.drawable.ill_one_inch_powered), - contentDescription = null, - contentScale = ContentScale.Fit, - ) - Column { AppBarWithBackButton( text = stringResource(R.string.common_swap), onBackClick = state.onBackClicked, iconRes = R.drawable.ic_close_24, ) + Column( modifier = Modifier .fillMaxWidth() @@ -98,7 +88,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, onPermissionWarningClick: }, ) } - MainButton(state = state, onPermissionWarningClick = onPermissionWarningClick) + MainButton(state = state, onPermissionWarningClick = state.onShowPermissionBottomSheet) } } @@ -486,7 +476,7 @@ private val state = SwapStateHolder( @Composable private fun SwapScreenContentPreview() { TangemTheme(isDark = false) { - SwapScreenContent(state = state) {} + SwapScreenContent(state = state, modifier = Modifier) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index cef1abb918..15fa430b17 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -133,7 +133,6 @@ internal class SwapViewModel @Inject constructor( fun setRouter(router: SwapRouter) { swapRouter = router uiState = uiState.copy( - onBackClicked = router::back, onSelectTokenClick = { router.openScreen(SwapNavScreen.SelectToken) analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened) @@ -260,31 +259,7 @@ internal class SwapViewModel @Inject constructor( }, onSuccess = { providersState -> val (provider, state) = updateLoadedQuotes(providersState) - when (state) { - is SwapState.QuotesLoadedState -> { - fillDataState(state.permissionState, state.swapDataModel) - uiState = stateBuilder.createQuotesLoadedState( - uiStateHolder = uiState, - quoteModel = state, - fromToken = fromToken.currency, - swapProvider = provider, - ) { updatedFee -> - dataState = dataState.copy( - selectedFee = updatedFee, - ) - } - } - is SwapState.EmptyAmountState -> { - uiState = stateBuilder.createQuotesEmptyAmountState( - uiStateHolder = uiState, - emptyAmountState = state, - ) - } - is SwapState.SwapError -> { - Timber.e("SwapError when loading quotes ${state.error}") - uiState = stateBuilder.mapError(uiState, state.error) { startLoadingQuotesFromLastState() } - } - } + setupLoadedState(provider, state, fromToken) }, onError = { Timber.e("Error when loading quotes: $it") @@ -293,6 +268,34 @@ internal class SwapViewModel @Inject constructor( ) } + private fun setupLoadedState(provider: SwapProvider, state: SwapState, fromToken: CryptoCurrencyStatus) { + when (state) { + is SwapState.QuotesLoadedState -> { + fillDataState(state.permissionState, state.swapDataModel) + uiState = stateBuilder.createQuotesLoadedState( + uiStateHolder = uiState, + quoteModel = state, + fromToken = fromToken.currency, + swapProvider = provider, + ) { updatedFee -> + dataState = dataState.copy( + selectedFee = updatedFee, + ) + } + } + is SwapState.EmptyAmountState -> { + uiState = stateBuilder.createQuotesEmptyAmountState( + uiStateHolder = uiState, + emptyAmountState = state, + ) + } + is SwapState.SwapError -> { + Timber.e("SwapError when loading quotes ${state.error}") + uiState = stateBuilder.mapError(uiState, state.error) { startLoadingQuotesFromLastState() } + } + } + } + private fun updateLoadedQuotes(state: Map): Pair { val selectedSwapProvider = dataState.selectedProvider ?: state.keys.first() dataState = dataState.copy( @@ -563,11 +566,20 @@ internal class SwapViewModel @Inject constructor( onChangeCardsClicked() analyticsEventHandler.send(SwapEvents.ButtonSwipeClicked) }, - onBackClicked = { onSearchEntered("") }, + onBackClicked = { + val bottomSheet = uiState.bottomSheetConfig + if (bottomSheet != null && bottomSheet.isShow) { + uiState = stateBuilder.dismissBottomSheet(uiState) + } else { + swapRouter.back() + } + onSearchEntered("") + }, onMaxAmountSelected = { onMaxAmountClicked() }, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) + uiState = stateBuilder.showPermissionBottomSheet(uiState) { stateBuilder.dismissBottomSheet(uiState) } }, hidePermissionBottomSheet = { startLoadingQuotesFromLastState() @@ -597,9 +609,38 @@ internal class SwapViewModel @Inject constructor( }, onClickFee = {}, onSelectFeeType = {}, + onProviderClick = { + uiState = stateBuilder.showSelectProviderBottomSheet( + uiState = uiState, + selectedProviderId = it, + providersStates = dataState.lastLoadedSwapStates, + ) { uiState = stateBuilder.dismissBottomSheet(uiState) } + }, + onProviderSelect = { + val provider = findAndSelectProvider(it) + val swapState = dataState.lastLoadedSwapStates[provider] + val fromToken = dataState.fromCryptoCurrency + if (provider != null && swapState != null && fromToken != null) { + setupLoadedState( + provider = provider, + state = swapState, + fromToken = fromToken, + ) + } + }, ) } + private fun findAndSelectProvider(providerId: String): SwapProvider? { + val selectedProvider = dataState.lastLoadedSwapStates.keys.firstOrNull { it.providerId == providerId } + if (selectedProvider != null) { + dataState = dataState.copy( + selectedProvider = selectedProvider, + ) + } + return selectedProvider + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> From c4b1dceb65107f96962cf42e785fc50856bdbea6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Nov 2023 22:19:24 +0200 Subject: [PATCH 040/139] Updated on 2026-08-14 --- .../tangem/tap/proxy/UserWalletManagerImpl.kt | 26 +++++++-- features/learn2earn/impl/build.gradle.kts | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 58 +++++++------------ .../swap/domain/di/SwapDomainModule.kt | 2 + libs/crypto/build.gradle.kts | 6 +- .../tangem/lib/crypto/UserWalletManager.kt | 3 +- 6 files changed, 50 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 74fce7b587..d53af693db 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -6,10 +6,13 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager import com.tangem.common.doOnFailure import com.tangem.common.extensions.guard +import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.lib.crypto.UserWalletManager @@ -20,6 +23,7 @@ import com.tangem.lib.crypto.models.ProxyAmount import com.tangem.lib.crypto.models.ProxyFiatCurrency import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store import com.tangem.tap.userWalletsListManager import com.tangem.tap.walletCurrenciesManager import com.tangem.tap.walletStoresManager @@ -34,6 +38,8 @@ class UserWalletManagerImpl( private val walletFeatureToggles: WalletFeatureToggles, ) : UserWalletManager { + val cryptoCurrencyFactory = CryptoCurrencyFactory() + override suspend fun getUserTokens( networkId: String, derivationPath: String?, @@ -78,13 +84,21 @@ class UserWalletManagerImpl( } } - override fun getNativeTokenForNetwork(networkId: String): Currency { + override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - return NativeToken( - id = blockchain.toCoinId(), - name = blockchain.fullName, - symbol = blockchain.currency, - networkId = networkId, + + return requireNotNull( + cryptoCurrencyFactory.createCoin( + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = requireNotNull( + store.state.globalState + .userWalletsListManager + ?.selectedUserWalletSync + ?.scanResponse + ?.derivationStyleProvider, + ), + ) ) } diff --git a/features/learn2earn/impl/build.gradle.kts b/features/learn2earn/impl/build.gradle.kts index b9fb7f48a6..dcb142ae2d 100644 --- a/features/learn2earn/impl/build.gradle.kts +++ b/features/learn2earn/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(project(":data:common")) implementation(project(":libs:auth")) implementation(project(":libs:crypto")) + implementation(projects.domain.tokens.models) implementation(deps.material) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 28de49180c..31e5dbaf5e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -6,8 +6,10 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.cache.SwapDataCache @@ -44,6 +46,7 @@ internal class SwapInteractorImpl @Inject constructor( private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val quotesRepository: QuotesRepository, ) : SwapInteractor { // TODO: Move to DI @@ -389,32 +392,6 @@ internal class SwapInteractorImpl @Inject constructor( } } - @Suppress("UnusedPrivateMember") - @Deprecated("used in old swap mechanism") - private fun getTokensWithBalance( - tokens: List, - balances: Map, - rates: Map, - appCurrency: ProxyFiatCurrency, - ): List { - return tokens.map { - val balance = balances[it.symbol] - TokenWithBalance( - token = it, - tokenBalanceData = TokenBalanceData( - amount = balance?.let { amount -> - amountFormatter.formatSwapAmountToUI(amount, it.symbol) - }, - amountEquivalent = balance?.value?.toFiatString( - rateValue = rates[it.id]?.toBigDecimal() ?: BigDecimal.ZERO, - fiatCurrencyName = appCurrency.symbol, - formatWithSpaces = true, - ), - ), - ) - } - } - private suspend fun isAllowedToSpend(networkId: String, fromToken: CryptoCurrency, amount: SwapAmount): Boolean { if (fromToken is CryptoCurrency.Coin) return true return getSelectedWalletSyncUseCase().fold( @@ -538,8 +515,8 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun getFormattedFiatFees(networkId: String, vararg fees: BigDecimal): List { val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) - val rates = repository.getRates(appCurrency.code, listOf(nativeToken.id)) - return rates[nativeToken.id]?.toBigDecimal()?.let { rate -> + val rates = getQuotes(nativeToken.id) + return rates[nativeToken.id]?.fiatRate?.let { rate -> fees.map { fee -> " (${fee.toFiatString(rate, appCurrency.symbol, true)})" } @@ -629,28 +606,26 @@ internal class SwapInteractorImpl @Inject constructor( val toToken = toTokenStatus.currency val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) - val rates = repository.getRates( - appCurrency.code, - listOf(fromToken.network.backendId, toToken.network.backendId, nativeToken.id), - ) + + val rates = getQuotes(fromToken.id, toToken.id, nativeToken.id) return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( tokenAmount = fromTokenAmount, cryptoCurrencyStatus = fromTokenStatus, - amountFiat = rates[fromToken.network.backendId]?.toBigDecimal()?.multiply(fromTokenAmount.value) + amountFiat = rates[fromToken.id]?.fiatRate?.multiply(fromTokenAmount.value) ?: BigDecimal.ZERO, ), toTokenInfo = TokenSwapInfo( tokenAmount = toTokenAmount, cryptoCurrencyStatus = toTokenStatus, - amountFiat = rates[toToken.network.backendId]?.toBigDecimal()?.multiply(toTokenAmount.value) + amountFiat = rates[toToken.id]?.fiatRate?.multiply(toTokenAmount.value) ?: BigDecimal.ZERO, ), priceImpact = calculatePriceImpact( fromTokenAmount = fromTokenAmount.value, - fromRate = rates[fromToken.network.backendId] ?: 0.0, + fromRate = rates[fromToken.id]?.fiatRate?.toDouble() ?: 0.0, toTokenAmount = toTokenAmount.value, - toRate = rates[toToken.network.backendId] ?: 0.0, + toRate = rates[toToken.id]?.fiatRate?.toDouble() ?: 0.0, ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), swapDataModel = swapStateData, @@ -687,7 +662,7 @@ internal class SwapInteractorImpl @Inject constructor( val feeData = transactionManager.getFee( networkId = networkId, amountToSend = BigDecimal.ZERO, - currencyToSend = userWalletManager.getNativeTokenForNetwork(networkId), + currencyToSend = swapCurrencyConverter.convert(userWalletManager.getNativeTokenForNetwork(networkId)), destinationAddress = getTokenAddress(fromToken), increaseBy = INCREASE_GAS_LIMIT_BY, data = transactionData, @@ -863,6 +838,15 @@ internal class SwapInteractorImpl @Inject constructor( ) } + private suspend fun getQuotes(vararg ids: CryptoCurrency.ID) : Map { + val set = quotesRepository.getQuotesSync(ids.toSet(), false) + + return ids + .mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } } + .toMap() + } + + companion object { private const val DEFAULT_SLIPPAGE = 2 diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 8845c68949..bdcfad48f9 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -35,6 +35,7 @@ class SwapDomainModule { walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + quotesRepository: QuotesRepository, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -47,6 +48,7 @@ class SwapDomainModule { walletFeatureToggles = walletFeatureToggles, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, + quotesRepository = quotesRepository, ) } diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index 248cc20bd1..f8a413d31d 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -1,10 +1,12 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } dependencies { - /** Coroutines */ implementation(deps.kotlin.coroutines) + implementation(projects.domain.tokens.models) + } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index 0a489b3498..c18bc06e73 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -1,5 +1,6 @@ package com.tangem.lib.crypto +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.ProxyAmount import com.tangem.lib.crypto.models.ProxyFiatCurrency @@ -16,7 +17,7 @@ interface UserWalletManager { suspend fun getUserTokens(networkId: String, derivationPath: String?, isExcludeCustom: Boolean): List @Throws(IllegalStateException::class) - fun getNativeTokenForNetwork(networkId: String): Currency + fun getNativeTokenForNetwork(networkId: String): CryptoCurrency /** * Returns user walletId or empty string From 8ff70e46159afcfa5bf5fc3a863afd4fd2a6c52f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 22 Nov 2023 22:32:02 +0200 Subject: [PATCH 041/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractor.kt | 8 -------- .../feature/swap/domain/SwapInteractorImpl.kt | 8 -------- .../feature/swap/domain/cache/SwapDataCache.kt | 6 ++---- .../swap/domain/cache/SwapDataCacheImpl.kt | 18 ------------------ 4 files changed, 2 insertions(+), 38 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 4a196f0569..9a6d547675 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -25,14 +25,6 @@ interface SwapInteractor { */ suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensStateExpress - /** - * Find specific token by id, null if not found - * - * @param id token id - * @return [CryptoCurrency] or null - */ - fun findTokenById(id: String): CryptoCurrency? - /** * Gives permission to swap, this starts scan card process * diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 31e5dbaf5e..d0e6bbf971 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -190,14 +190,6 @@ internal class SwapInteractorImpl @Inject constructor( ) } - @Deprecated("used in old swap mechanism") - override fun findTokenById(id: String): CryptoCurrency? { - val tokensInWallet = cache.getInWalletTokens() - val loadedTokens = cache.getLoadedTokens() - return tokensInWallet.firstOrNull { it.token.id.value == id }?.token - ?: loadedTokens.firstOrNull { it.token.id.value == id }?.token - } - @Deprecated("used in old swap mechanism") override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState { val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt index 4890b5ca49..5370981455 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt @@ -7,14 +7,12 @@ import java.math.BigDecimal interface SwapDataCache { - fun cacheAvailableToSwapTokens(networkId: String, tokens: List) fun cacheInWalletTokens(tokens: List) fun cacheLoadedTokens(tokens: List) fun cacheBalances(networkId: String, derivationPath: String?, balances: Map) - fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String) - fun getAvailableTokens(networkId: String): List + + fun getInWalletTokens(): List fun getLoadedTokens(): List fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? - fun getLastFeeForNetwork(networkId: String): BigDecimal? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt index 2a5d7535ca..7461c206b0 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt @@ -7,16 +7,10 @@ import java.math.BigDecimal class SwapDataCacheImpl : SwapDataCache { - private val availableTokensForNetwork: MutableMap> = mutableMapOf() - private val feesForNetworks: MutableMap = mutableMapOf() private val tokensBalances: MutableMap> = mutableMapOf() private val lastInWalletTokens = mutableListOf() private val lastLoadedTokens = mutableListOf() - override fun cacheLastFeeForNetwork(fee: BigDecimal, networkId: String) { - feesForNetworks[networkId] = fee - } - override fun cacheInWalletTokens(tokens: List) { lastInWalletTokens.clear() lastInWalletTokens.addAll(tokens) @@ -43,18 +37,6 @@ class SwapDataCacheImpl : SwapDataCache { tokensBalances[createKeyFrom(networkId, derivationPath)] = balances } - override fun cacheAvailableToSwapTokens(networkId: String, tokens: List) { - availableTokensForNetwork[networkId] = tokens - } - - override fun getLastFeeForNetwork(networkId: String): BigDecimal? { - return feesForNetworks[networkId] - } - - override fun getAvailableTokens(networkId: String): List { - return availableTokensForNetwork.getOrElse(networkId) { emptyList() } - } - private fun createKeyFrom(networkId: String, derivationPath: String?): String { return "$networkId;$derivationPath" } From 91415eb0d953bab9515cd0fe4544ca5c47bec951 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 23 Nov 2023 12:03:56 +0200 Subject: [PATCH 042/139] Updated on 2026-08-14 --- .../tangem/tap/proxy/UserWalletManagerImpl.kt | 22 +++++------------- features/swap/data/build.gradle.kts | 2 ++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 23 +++++++++++++++++++ .../tangem/feature/swap/di/SwapDataModule.kt | 4 ++++ .../feature/swap/domain/SwapInteractorImpl.kt | 7 +++--- .../feature/swap/domain/SwapRepository.kt | 2 ++ .../tangem/lib/crypto/UserWalletManager.kt | 2 +- 7 files changed, 41 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index d53af693db..0fb5fb8a77 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -38,8 +38,6 @@ class UserWalletManagerImpl( private val walletFeatureToggles: WalletFeatureToggles, ) : UserWalletManager { - val cryptoCurrencyFactory = CryptoCurrencyFactory() - override suspend fun getUserTokens( networkId: String, derivationPath: String?, @@ -84,21 +82,13 @@ class UserWalletManagerImpl( } } - override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency { + override fun getNativeTokenForNetwork(networkId: String): Currency { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - - return requireNotNull( - cryptoCurrencyFactory.createCoin( - blockchain = blockchain, - extraDerivationPath = null, - derivationStyleProvider = requireNotNull( - store.state.globalState - .userWalletsListManager - ?.selectedUserWalletSync - ?.scanResponse - ?.derivationStyleProvider, - ), - ) + return NativeToken( + id = blockchain.toCoinId(), + name = blockchain.fullName, + symbol = blockchain.currency, + networkId = networkId, ) } diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 67d016525d..2d753c6fa2 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) /** Data */ @@ -32,5 +33,6 @@ dependencies { /** DI */ implementation(deps.hilt.android) + kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index f63f42e5f3..88931b2e71 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Approver import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.extensions.Result +import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.request.PairsRequestBody @@ -15,8 +16,11 @@ import com.tangem.datasource.api.oneinch.errors.OneIncResponseException import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.config.ConfigManager import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.SwapRepository @@ -40,6 +44,7 @@ internal class SwapRepositoryImpl @Inject constructor( private val coroutineDispatcher: CoroutineDispatcherProvider, private val configManager: ConfigManager, private val walletManagersFacade: WalletManagersFacade, + private val walletsStateHolder: WalletsStateHolder, ) : SwapRepository { private val tokensConverter = TokensConverter() @@ -47,6 +52,7 @@ internal class SwapRepositoryImpl @Inject constructor( private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() private val swapProviderConverter = SwapProviderConverter() + private val cryptoCurrencyFactory = CryptoCurrencyFactory() override suspend fun getPairs( initialCurrency: LeastTokenInfo, @@ -304,6 +310,23 @@ internal class SwapRepositoryImpl @Inject constructor( ) } + override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency { + val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + + return requireNotNull( + cryptoCurrencyFactory.createCoin( + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = requireNotNull( + walletsStateHolder.userWalletsListManager + ?.selectedUserWalletSync + ?.scanResponse + ?.derivationStyleProvider, + ), + ) + ) + } + companion object { // TODO("get this ids from blockchain enum later") private const val OPTIMISM_ID = "optimistic-ethereum" diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 89506db425..729564de7c 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -6,6 +6,8 @@ import com.tangem.datasource.api.oneinch.OneInchErrorsHandler import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.config.ConfigManager import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.feature.swap.SwapRepositoryImpl import com.tangem.feature.swap.domain.SwapRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -29,6 +31,7 @@ class SwapDataModule { coroutineDispatcher: CoroutineDispatcherProvider, configManager: ConfigManager, walletManagerFacade: WalletManagersFacade, + walletsStateHolder: WalletsStateHolder ): SwapRepository { return SwapRepositoryImpl( tangemTechApi = tangemTechApi, @@ -38,6 +41,7 @@ class SwapDataModule { coroutineDispatcher = coroutineDispatcher, configManager = configManager, walletManagersFacade = walletManagerFacade, + walletsStateHolder = walletsStateHolder, ) } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index d0e6bbf971..19c8601f2a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -506,7 +506,7 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun getFormattedFiatFees(networkId: String, vararg fees: BigDecimal): List { val appCurrency = userWalletManager.getUserAppCurrency() - val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) + val nativeToken = repository.getNativeTokenForNetwork(networkId) val rates = getQuotes(nativeToken.id) return rates[nativeToken.id]?.fiatRate?.let { rate -> fees.map { fee -> @@ -596,8 +596,7 @@ internal class SwapInteractorImpl @Inject constructor( ): SwapState.QuotesLoadedState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency - val appCurrency = userWalletManager.getUserAppCurrency() - val nativeToken = userWalletManager.getNativeTokenForNetwork(networkId) + val nativeToken = repository.getNativeTokenForNetwork(networkId) val rates = getQuotes(fromToken.id, toToken.id, nativeToken.id) return SwapState.QuotesLoadedState( @@ -654,7 +653,7 @@ internal class SwapInteractorImpl @Inject constructor( val feeData = transactionManager.getFee( networkId = networkId, amountToSend = BigDecimal.ZERO, - currencyToSend = swapCurrencyConverter.convert(userWalletManager.getNativeTokenForNetwork(networkId)), + currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)), destinationAddress = getTokenAddress(fromToken), increaseBy = INCREASE_GAS_LIMIT_BY, data = transactionData, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index f8664afb69..60d4275364 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -78,4 +78,6 @@ interface SwapRepository { providerId: String, rateType: RateType, ): ExchangeQuote + + fun getNativeTokenForNetwork(networkId: String): CryptoCurrency } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index c18bc06e73..2006764b47 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -17,7 +17,7 @@ interface UserWalletManager { suspend fun getUserTokens(networkId: String, derivationPath: String?, isExcludeCustom: Boolean): List @Throws(IllegalStateException::class) - fun getNativeTokenForNetwork(networkId: String): CryptoCurrency + fun getNativeTokenForNetwork(networkId: String): Currency /** * Returns user walletId or empty string From b5dbec5350711e6e7af4888f274121ffbe3511f8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 23 Nov 2023 12:08:17 +0200 Subject: [PATCH 043/139] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt | 4 ---- .../main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt | 3 +-- .../main/java/com/tangem/feature/swap/di/SwapDataModule.kt | 3 +-- .../com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 6 ++---- .../com/tangem/feature/swap/domain/cache/SwapDataCache.kt | 3 --- .../tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt | 2 -- .../main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 2 +- .../src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt | 4 ---- .../com/tangem/feature/swap/viewmodels/SwapViewModel.kt | 2 +- .../main/java/com/tangem/lib/crypto/UserWalletManager.kt | 1 - 10 files changed, 6 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 0fb5fb8a77..74fce7b587 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -6,13 +6,10 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager import com.tangem.common.doOnFailure import com.tangem.common.extensions.guard -import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.lib.crypto.UserWalletManager @@ -23,7 +20,6 @@ import com.tangem.lib.crypto.models.ProxyAmount import com.tangem.lib.crypto.models.ProxyFiatCurrency import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.store import com.tangem.tap.userWalletsListManager import com.tangem.tap.walletCurrenciesManager import com.tangem.tap.walletStoresManager diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 88931b2e71..a3d9b7aa92 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -19,7 +19,6 @@ import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* @@ -323,7 +322,7 @@ internal class SwapRepositoryImpl @Inject constructor( ?.scanResponse ?.derivationStyleProvider, ), - ) + ), ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 729564de7c..a70de3e545 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -6,7 +6,6 @@ import com.tangem.datasource.api.oneinch.OneInchErrorsHandler import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.config.ConfigManager import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.feature.swap.SwapRepositoryImpl import com.tangem.feature.swap.domain.SwapRepository @@ -31,7 +30,7 @@ class SwapDataModule { coroutineDispatcher: CoroutineDispatcherProvider, configManager: ConfigManager, walletManagerFacade: WalletManagersFacade, - walletsStateHolder: WalletsStateHolder + walletsStateHolder: WalletsStateHolder, ): SwapRepository { return SwapRepositoryImpl( tangemTechApi = tangemTechApi, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 19c8601f2a..a43fd0295d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -17,7 +17,6 @@ import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* -import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles @@ -597,7 +596,7 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency val nativeToken = repository.getNativeTokenForNetwork(networkId) - + val rates = getQuotes(fromToken.id, toToken.id, nativeToken.id) return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( @@ -829,7 +828,7 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun getQuotes(vararg ids: CryptoCurrency.ID) : Map { + private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { val set = quotesRepository.getQuotesSync(ids.toSet(), false) return ids @@ -837,7 +836,6 @@ internal class SwapInteractorImpl @Inject constructor( .toMap() } - companion object { private const val DEFAULT_SLIPPAGE = 2 diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt index 5370981455..ce4956ba90 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt @@ -1,9 +1,7 @@ package com.tangem.feature.swap.domain.cache import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress -import java.math.BigDecimal interface SwapDataCache { @@ -11,7 +9,6 @@ interface SwapDataCache { fun cacheLoadedTokens(tokens: List) fun cacheBalances(networkId: String, derivationPath: String?, balances: Map) - fun getInWalletTokens(): List fun getLoadedTokens(): List fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt index 7461c206b0..92c051e246 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt @@ -1,9 +1,7 @@ package com.tangem.feature.swap.domain.cache import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.Currency import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress -import java.math.BigDecimal class SwapDataCacheImpl : SwapDataCache { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 4089832235..4033c21dbc 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -34,7 +34,7 @@ import java.math.RoundingMode /** * State builder creates a specific states for SwapScreen */ -@Suppress("LargeClass") +@Suppress("LargeClass", "TooManyFunctions") internal class StateBuilder( private val actions: UiActions, private val isBalanceHiddenProvider: Provider, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 692d6a4e57..9d59406885 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -2,19 +2,15 @@ package com.tangem.feature.swap.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.padding import androidx.compose.material.* import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig -import com.tangem.feature.swap.presentation.R @OptIn(ExperimentalLayoutApi::class) @Composable diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 15fa430b17..cca50c074d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -530,7 +530,7 @@ internal class SwapViewModel @Inject constructor( } } - @Suppress("LongMethod") + @Suppress("LongMethod", "CyclomaticComplexMethod") private fun createUiActions(): UiActions { return UiActions( onSearchEntered = { onSearchEntered(it) }, diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index 2006764b47..0a489b3498 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -1,6 +1,5 @@ package com.tangem.lib.crypto -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.ProxyAmount import com.tangem.lib.crypto.models.ProxyFiatCurrency From 464f8f8a80a6ca06d70d54c5c993b53f2c9ec48d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 23 Nov 2023 15:29:21 +0300 Subject: [PATCH 044/139] Updated on 2026-08-14 --- .../models/response/SwapPairsWithProviders.kt | 6 ++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 30 +++++----- .../swap/converters/SwapPairInfoConverter.kt | 55 ++++++++++++------- .../swap/converters/SwapProviderConverter.kt | 25 --------- .../feature/swap/domain/SwapInteractorImpl.kt | 11 +--- .../feature/swap/domain/SwapRepository.kt | 2 - .../domain/models/domain/SwapPairLeast.kt | 6 +- .../states/ChooseProviderBottomSheetConfig.kt | 2 +- .../swap/ui/ChooseProviderBottomSheet.kt | 26 ++++++--- .../tangem/feature/swap/ui/ProviderItem.kt | 14 ++--- .../tangem/feature/swap/ui/StateBuilder.kt | 28 ++++++++-- .../com/tangem/feature/swap/ui/SwapScreen.kt | 2 - .../feature/swap/ui/SwapScreenContent.kt | 9 ++- .../feature/swap/viewmodels/SwapViewModel.kt | 1 + 14 files changed, 115 insertions(+), 102 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPairsWithProviders.kt delete mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPairsWithProviders.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPairsWithProviders.kt new file mode 100644 index 0000000000..41568fb92f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPairsWithProviders.kt @@ -0,0 +1,6 @@ +package com.tangem.datasource.api.express.models.response + +class SwapPairsWithProviders( + val swapPair: List, + val providers: List, +) \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index a3d9b7aa92..72b4687f50 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -9,6 +9,8 @@ import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.request.PairsRequestBody +import com.tangem.datasource.api.express.models.response.SwapPair +import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders import com.tangem.datasource.api.oneinch.OneInchApi import com.tangem.datasource.api.oneinch.OneInchApiFactory import com.tangem.datasource.api.oneinch.OneInchErrorsHandler @@ -50,7 +52,6 @@ internal class SwapRepositoryImpl @Inject constructor( private val swapConverter = SwapConverter() private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() - private val swapProviderConverter = SwapProviderConverter() private val cryptoCurrencyFactory = CryptoCurrencyFactory() override suspend fun getPairs( @@ -78,34 +79,29 @@ internal class SwapRepositoryImpl @Inject constructor( ) } - pairs.await() + reversedPairs.await() - } - } + val allPairs = pairs.await() + reversedPairs.await() - override suspend fun getProvidersDetails(providers: Set): List { - val providersMap = providers.associateBy { it.providerId } - return tangemExpressApi.getProviders().getOrThrow().mapNotNull { - val provider = providersMap[it.id] - if (provider != null) { - swapProviderConverter.convert(it).copy(rateTypes = provider.rateTypes) - } else { - null - } + val providers = tangemExpressApi.getProviders().getOrThrow() + + return@withContext swapPairInfoConverter.convert( + SwapPairsWithProviders( + swapPair = allPairs, + providers = providers, + ), + ) } } private suspend fun getPairsInternal( from: List, to: List, - ): List { + ): List { return tangemExpressApi.getPairs( PairsRequestBody( from = from, to = to, ), - ) - .getOrThrow() - .map { swapPairInfoConverter.convert(it) } + ).getOrThrow() } override suspend fun getRates(currencyId: String, tokenIds: List): Map { diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index c205bec15e..ff86a7e049 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -1,36 +1,53 @@ package com.tangem.feature.swap.converters -import com.tangem.datasource.api.express.models.response.SwapPair -import com.tangem.datasource.api.express.models.response.SwapPairProvider +import com.tangem.datasource.api.express.models.response.* import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo +import com.tangem.utils.converter.Converter +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType as ExchangeProviderTypeDomain import com.tangem.feature.swap.domain.models.domain.SwapPairLeast as SwapPairDomain import com.tangem.feature.swap.domain.models.domain.SwapProvider as SwapPairProviderDomain -import com.tangem.utils.converter.Converter -class SwapPairInfoConverter : Converter { +class SwapPairInfoConverter : Converter> { private val rateTypeConverter = RateTypeConverter() - override fun convert(value: SwapPair): SwapPairDomain { - return SwapPairDomain( - from = LeastTokenInfo( - contractAddress = value.from.contractAddress, - network = value.from.network, - ), - to = LeastTokenInfo( - contractAddress = value.to.contractAddress, - network = value.to.network, - ), - providers = value.providers.map { - convertProvider(it) - }, - ) + override fun convert(value: SwapPairsWithProviders): List { + val providersAdditionalMap = value.providers.associateBy { it.id } + return value.swapPair.map { pair -> + SwapPairDomain( + from = LeastTokenInfo( + contractAddress = pair.from.contractAddress, + network = pair.from.network, + ), + to = LeastTokenInfo( + contractAddress = pair.to.contractAddress, + network = pair.to.network, + ), + providers = pair.providers.mapNotNull { + convertProvider(it, providersAdditionalMap) + }, + ) + } } - private fun convertProvider(swapPairProvider: SwapPairProvider): SwapPairProviderDomain { + private fun convertProvider( + swapPairProvider: SwapPairProvider, + providerAdditional: Map, + ): SwapPairProviderDomain? { + val additionalProvider = providerAdditional[swapPairProvider.providerId] ?: return null return SwapPairProviderDomain( providerId = swapPairProvider.providerId, rateTypes = swapPairProvider.rateTypes.map { rateTypeConverter.convert(it) }, + name = additionalProvider.name, + type = convertExchangeType(additionalProvider.type), + imageLarge = additionalProvider.imageLargeUrl, ) } + + private fun convertExchangeType(type: ExchangeProviderType): ExchangeProviderTypeDomain { + return when (type) { + ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX + ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX + } + } } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt deleted file mode 100644 index 6930923128..0000000000 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapProviderConverter.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.feature.swap.converters - -import com.tangem.datasource.api.express.models.response.ExchangeProvider -import com.tangem.datasource.api.express.models.response.ExchangeProviderType -import com.tangem.feature.swap.domain.models.domain.SwapProvider -import com.tangem.utils.converter.Converter -import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType as ExchangeProviderTypeDomain - -class SwapProviderConverter : Converter { - override fun convert(value: ExchangeProvider): SwapProvider { - return SwapProvider( - providerId = value.id, - name = value.name, - type = convertExchangeType(value.type), - imageLarge = value.imageLargeUrl, - ) - } - - private fun convertExchangeType(type: ExchangeProviderType): ExchangeProviderTypeDomain { - return when (type) { - ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX - ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX - } - } -} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index a43fd0295d..5c185a5cff 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -152,16 +152,7 @@ internal class SwapInteractorImpl @Inject constructor( initialCurrency: LeastTokenInfo, currenciesList: List, ): List { - val pairs = repository.getPairs(initialCurrency, currenciesList) - val providers = pairs.flatMap { it.providers }.toSet() - val updatedProviders = repository.getProvidersDetails(providers).associateBy { it.providerId } - return pairs.map { pair -> - pair.copy( - providers = pair.providers.mapNotNull { currentProvider -> - updatedProviders[currentProvider.providerId] - }, - ) - } + return repository.getPairs(initialCurrency, currenciesList) } @Deprecated("used in old swap mechanism") diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 60d4275364..31863dc852 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -10,8 +10,6 @@ interface SwapRepository { suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List): List - suspend fun getProvidersDetails(providers: Set): List - suspend fun getRates(currencyId: String, tokenIds: List): Map suspend fun getExchangeableTokens(networkId: String): List diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index 76c2c0795d..76fa9a8238 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -29,9 +29,9 @@ data class CryptoCurrencySwapInfo( data class SwapProvider( val providerId: String, val rateTypes: List = emptyList(), - val name: String? = null, - val type: ExchangeProviderType? = null, - val imageLarge: String? = null, + val name: String, + val type: ExchangeProviderType, + val imageLarge: String, ) enum class ExchangeProviderType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt index 7507686111..68c761cd14 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt @@ -3,7 +3,7 @@ package com.tangem.feature.swap.models.states import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import kotlinx.collections.immutable.ImmutableList -class ChooseProviderBottomSheetConfig( +data class ChooseProviderBottomSheetConfig( val selectedProviderId: String, val providers: ImmutableList, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index e93e82c505..6d2bab0f4f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -1,12 +1,15 @@ package com.tangem.feature.swap.ui import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.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.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet @@ -52,17 +55,24 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC .background( color = TangemTheme.colors.background.action, shape = TangemTheme.shapes.roundedCornersXMedium, - ), + ) + .clip(shape = TangemTheme.shapes.roundedCornersXMedium), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - content.providers.forEach { - val isSelected = it.id == content.selectedProviderId + content.providers.forEach { provider -> + val isSelected = provider.id == content.selectedProviderId ProviderItem( - state = it, + state = provider, isSelected = isSelected, - modifier = Modifier.padding( - horizontal = TangemTheme.dimens.spacing12, - vertical = TangemTheme.dimens.spacing12, - ), + modifier = Modifier + .clickable( + enabled = provider.onProviderClick != null, + onClick = { provider.onProviderClick?.invoke(provider.id) }, + ) + .padding( + horizontal = TangemTheme.dimens.spacing12, + vertical = TangemTheme.dimens.spacing12, + ), ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 1cd636d3e7..5d75293da0 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.ui import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon @@ -35,9 +34,9 @@ private val GrayscaleColorFilter: ColorFilter get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) @Composable -fun ProviderItemBlock(state: ProviderState) { +fun ProviderItemBlock(state: ProviderState, modifier: Modifier = Modifier) { if (state !is ProviderState.Empty) { - BaseContainer(state) { + BaseContainer(modifier = modifier) { ProviderItem( state = state, modifier = Modifier.align(Alignment.CenterStart), @@ -279,17 +278,14 @@ private fun BoxScope.ProviderChevron(state: ProviderState.Content, isSelected: B } @Composable -private fun BaseContainer(state: ProviderState, content: @Composable BoxScope.() -> Unit) { +private fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { Box( - modifier = Modifier + modifier = modifier .background( color = TangemTheme.colors.background.action, shape = TangemTheme.shapes.roundedCornersXMedium, ) - .clickable( - enabled = state.onProviderClick != null, - onClick = { state.onProviderClick?.invoke(state.id) }, - ) + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) .fillMaxWidth() .defaultMinSize(minHeight = TangemTheme.dimens.size68), ) { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 4033c21dbc..e7623069ab 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -270,6 +270,7 @@ internal class StateBuilder( providerState = swapProvider.convertToContentClickableProviderState( fromTokenInfo = quoteModel.fromTokenInfo, toTokenInfo = quoteModel.toTokenInfo, + selectionType = ProviderState.SelectionType.CLICK, onProviderClick = actions.onProviderClick, ), ) @@ -759,6 +760,21 @@ internal class StateBuilder( ) } + fun updateSelectedProvider(uiState: SwapStateHolder, selectedProviderId: String): SwapStateHolder { + val config = uiState.bottomSheetConfig?.content as? ChooseProviderBottomSheetConfig + return if (config != null) { + uiState.copy( + bottomSheetConfig = uiState.bottomSheetConfig.copy( + content = config.copy( + selectedProviderId = selectedProviderId, + ), + ), + ) + } else { + uiState + } + } + private fun Map.Entry.convertToProviderState( onProviderSelect: (String) -> Unit, ): ProviderState? { @@ -766,9 +782,10 @@ internal class StateBuilder( return when (val state = this.value) { is SwapState.EmptyAmountState -> null is SwapState.QuotesLoadedState -> provider.convertToContentClickableProviderState( - state.fromTokenInfo, - state.toTokenInfo, + fromTokenInfo = state.fromTokenInfo, + toTokenInfo = state.toTokenInfo, onProviderClick = onProviderSelect, + selectionType = ProviderState.SelectionType.SELECT, ) is SwapState.SwapError -> null } @@ -806,6 +823,7 @@ internal class StateBuilder( private fun SwapProvider.convertToContentClickableProviderState( fromTokenInfo: TokenSwapInfo, toTokenInfo: TokenSwapInfo, + selectionType: ProviderState.SelectionType, onProviderClick: (String) -> Unit, ): ProviderState { val rate = toTokenInfo.tokenAmount.value.divide( @@ -818,12 +836,12 @@ internal class StateBuilder( val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" return ProviderState.Content( id = this.providerId, - name = this.name ?: "", - iconUrl = this.imageLarge ?: "", + name = this.name, + iconUrl = this.imageLarge, type = this.type.toString(), rate = rateString, additionalBadge = ProviderState.AdditionalBadge.BestTrade, - selectionType = ProviderState.SelectionType.CLICK, + selectionType = selectionType, percentLowerThenBest = null, onProviderClick = onProviderClick, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 9d59406885..95003050c3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.ui import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.padding import androidx.compose.material.* import androidx.compose.material3.* @@ -12,7 +11,6 @@ import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig -@OptIn(ExperimentalLayoutApi::class) @Composable internal fun SwapScreen(stateHolder: SwapStateHolder) { BackHandler(onBack = stateHolder.onBackClicked) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 1ad3351901..59a9ab4cfd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -68,7 +68,14 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi ) { MainInfo(state) - ProviderItemBlock(state = state.providerState) + ProviderItemBlock( + state = state.providerState, + modifier = Modifier + .clickable( + enabled = state.providerState.onProviderClick != null, + onClick = { state.providerState.onProviderClick?.invoke(state.providerState.id) }, + ), + ) FeeItem(feeState = state.fee, currency = state.networkCurrency) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index cca50c074d..1d119785f8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -621,6 +621,7 @@ internal class SwapViewModel @Inject constructor( val swapState = dataState.lastLoadedSwapStates[provider] val fromToken = dataState.fromCryptoCurrency if (provider != null && swapState != null && fromToken != null) { + uiState = stateBuilder.updateSelectedProvider(uiState, provider.providerId) setupLoadedState( provider = provider, state = swapState, From 63e52abd27ba9020b2a831f389ea9bafb98f8968 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Nov 2023 01:25:44 +0200 Subject: [PATCH 045/139] Updated on 2026-08-14 --- .../api/express/TangemExpressApi.kt | 6 +- .../models/response/ExchangeDataResponse.kt | 11 ++- features/learn2earn/impl/build.gradle.kts | 1 - .../tangem/feature/swap/SwapRepositoryImpl.kt | 75 +++++++------------ .../swap/converters/ExpressDataConverter.kt | 41 ++++++++++ .../feature/swap/converters/SwapConverter.kt | 29 ------- .../feature/swap/domain/SwapInteractorImpl.kt | 30 +++++--- .../feature/swap/domain/SwapRepository.kt | 15 +--- .../feature/swap/domain/models/DataError.kt | 18 +---- .../models/data/AggregatedSwapDataModel.kt | 2 +- .../models/domain/ExpressTransactionModel.kt | 30 ++++++++ .../domain/models/domain/SwapDataModel.kt | 8 +- .../tangem/feature/swap/ui/StateBuilder.kt | 2 +- libs/crypto/build.gradle.kts | 6 +- 14 files changed, 141 insertions(+), 133 deletions(-) create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt delete mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 2d62fb3630..6144d6ad9e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -42,9 +42,9 @@ interface TangemExpressApi { @Query("fromNetwork") fromNetwork: String, @Query("toContractAddress") toContractAddress: String, @Query("toNetwork") toNetwork: String, - @Query("fromAmount") fromAmount: BigDecimal, - @Query("providerId") providerId: Int, - @Query("rateType") rateType: RateType, + @Query("fromAmount") fromAmount: String, + @Query("providerId") providerId: String, + @Query("rateType") rateType: String, @Query("toAddress") toAddress: String, ): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt index 3083bdfd0b..72deee1a57 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt @@ -4,8 +4,17 @@ import com.squareup.moshi.Json import java.math.BigDecimal data class ExchangeDataResponse( + @Json(name = "fromAmount") + val fromAmount: String, + + @Json(name = "fromDecimals") + val fromDecimals: Int, + @Json(name = "toAmount") - val toAmount: BigDecimal, + val toAmount: String, + + @Json(name = "toDecimals") + val toDecimals: Int, @Json(name = "txType") val txType: TxType, diff --git a/features/learn2earn/impl/build.gradle.kts b/features/learn2earn/impl/build.gradle.kts index dcb142ae2d..b9fb7f48a6 100644 --- a/features/learn2earn/impl/build.gradle.kts +++ b/features/learn2earn/impl/build.gradle.kts @@ -22,7 +22,6 @@ dependencies { implementation(project(":data:common")) implementation(project(":libs:auth")) implementation(project(":libs:crypto")) - implementation(projects.domain.tokens.models) implementation(deps.material) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 72b4687f50..3571784929 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -49,7 +49,7 @@ internal class SwapRepositoryImpl @Inject constructor( ) : SwapRepository { private val tokensConverter = TokensConverter() - private val swapConverter = SwapConverter() + private val expressDataConverter = ExpressDataConverter() private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() private val cryptoCurrencyFactory = CryptoCurrencyFactory() @@ -164,8 +164,8 @@ internal class SwapRepositoryImpl @Inject constructor( toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals), ), ) - } catch (ex: OneIncResponseException) { - AggregatedSwapDataModel(null, mapErrors(ex.data.description)) + } catch (ex: Exception) { + AggregatedSwapDataModel(null, mapErrors(ex.message)) } } } @@ -176,31 +176,33 @@ internal class SwapRepositoryImpl @Inject constructor( } } - override suspend fun prepareSwapTransaction( - networkId: String, - fromTokenAddress: String, - toTokenAddress: String, - amount: String, - fromWalletAddress: String, - slippage: Int, + override suspend fun getExchangeData( + fromContractAddress: String, + fromNetwork: String, + toContractAddress: String, + toNetwork: String, + fromAmount: String, + providerId: String, + rateType: RateType, + toAddress: String, ): AggregatedSwapDataModel { return withContext(coroutineDispatcher.io) { try { - val swapResponse = oneInchErrorsHandler.handleOneInchResponse( - getOneInchApi(networkId).swap( - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - amount = amount, - fromAddress = fromWalletAddress, - slippage = slippage, - referrerAddress = configManager.config.swapReferrerAccount?.address, - fee = configManager.config.swapReferrerAccount?.fee, - ), + val response = tangemExpressApi.getExchangeData( + fromContractAddress = fromContractAddress, + fromNetwork = fromNetwork, + toContractAddress = toContractAddress, + toNetwork = toNetwork, + fromAmount = fromAmount, + providerId = providerId, + rateType = rateType.name.lowercase(), + toAddress = toAddress + ).getOrThrow() + AggregatedSwapDataModel( + dataModel = expressDataConverter.convert(response) ) - - AggregatedSwapDataModel(swapConverter.convert(swapResponse)) - } catch (ex: OneIncResponseException) { - AggregatedSwapDataModel(null, mapErrors(ex.data.description)) + } catch (ex: Exception) { + AggregatedSwapDataModel(null, mapErrors(ex.message)) } } } @@ -280,31 +282,6 @@ internal class SwapRepositoryImpl @Inject constructor( return oneInchApiFactory.getApi(networkId) } - override suspend fun getExchangeQuote( - fromContractAddress: String, - fromNetwork: String, - toContractAddress: String, - toNetwork: String, - fromAmount: String, - providerId: String, - rateType: RateType, - ): ExchangeQuote { - val response = tangemExpressApi.getExchangeQuote( - fromContractAddress, - fromNetwork, - toContractAddress, - toNetwork, - fromAmount, - providerId, - rateType.name.lowercase(), - ).getOrThrow() - - return ExchangeQuote( - toAmount = response.toAmount, - allowanceContract = response.allowanceContract, - ) - } - override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt new file mode 100644 index 0000000000..0c4278683f --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.express.models.response.ExchangeDataResponse +import com.tangem.datasource.api.express.models.response.TxType +import com.tangem.feature.swap.domain.models.createFromAmountWithOffset +import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel +import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import com.tangem.utils.converter.Converter + +class ExpressDataConverter : Converter { + + override fun convert(value: ExchangeDataResponse): SwapDataModel { + return SwapDataModel( + toTokenAmount = createFromAmountWithOffset(value.toAmount, value.toDecimals), + transaction = convertTransaction(value), + ) + } + + private fun convertTransaction(transactionDto: ExchangeDataResponse): ExpressTransactionModel { + return if (transactionDto.txType == TxType.SWAP) { + ExpressTransactionModel.DEX( + fromAmount = createFromAmountWithOffset(transactionDto.fromAmount, transactionDto.fromDecimals), + toAmount = createFromAmountWithOffset(transactionDto.toAmount, transactionDto.toDecimals), + txId = transactionDto.txId, + txTo = transactionDto.txTo, + txFrom = requireNotNull(transactionDto.txFrom), + txData = requireNotNull(transactionDto.txData) + ) + } else { + ExpressTransactionModel.CEX( + fromAmount = createFromAmountWithOffset(transactionDto.fromAmount, transactionDto.fromDecimals), + toAmount = createFromAmountWithOffset(transactionDto.toAmount, transactionDto.toDecimals), + txId = transactionDto.txId, + txTo = transactionDto.txTo, + externalTxId = requireNotNull(transactionDto.externalTxId), + externalTxUrl = requireNotNull(transactionDto.externalTxUrl), + ) + } + } + +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt deleted file mode 100644 index 3c8c1003ec..0000000000 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.feature.swap.converters - -import com.tangem.datasource.api.oneinch.models.SwapResponse -import com.tangem.datasource.api.oneinch.models.TransactionDto -import com.tangem.feature.swap.domain.models.createFromAmountWithOffset -import com.tangem.feature.swap.domain.models.domain.SwapDataModel -import com.tangem.feature.swap.domain.models.domain.TransactionModel -import com.tangem.utils.converter.Converter - -class SwapConverter : Converter { - - override fun convert(value: SwapResponse): SwapDataModel { - return SwapDataModel( - toTokenAmount = createFromAmountWithOffset(value.toTokenAmount, value.toToken.decimals), - transaction = convertTransaction(value.transaction), - ) - } - - private fun convertTransaction(transactionDto: TransactionDto): TransactionModel { - return TransactionModel( - fromWalletAddress = transactionDto.fromAddress, - toWalletAddress = transactionDto.toAddress, - data = transactionDto.data, - value = transactionDto.value, - gasPrice = transactionDto.gasPrice, - gas = transactionDto.gas, - ) - } -} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 5c185a5cff..67be5cd7a5 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -286,8 +286,8 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend = swapCurrencyConverter.convert(currencyToSend), feeAmount = fee.feeValue, gasLimit = fee.gasLimit, - destinationAddress = swapStateData.swapModel.transaction.toWalletAddress, - dataToSign = swapStateData.swapModel.transaction.data, + destinationAddress = swapStateData.swapModel.transaction.txTo, + dataToSign = (swapStateData.swapModel.transaction as ExpressTransactionModel.DEX).txData, ), isSwap = true, derivationPath = derivationPath, @@ -518,13 +518,21 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, selectedFee: FeeType, ): SwapState { - repository.prepareSwapTransaction( - networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - amount = amount.toStringWithRightOffset(), - slippage = DEFAULT_SLIPPAGE, - fromWalletAddress = getWalletAddress(networkId), + repository.getExchangeData( + fromContractAddress = fromToken.currency.getContractAddress(), + fromNetwork = fromToken.currency.network.backendId, + toContractAddress = toToken.currency.getContractAddress(), + toNetwork = toToken.currency.network.backendId, + fromAmount = amount.toStringWithRightOffset(), + providerId = "1", //TODO + rateType = RateType.FLOAT, + toAddress = toToken.value.networkAddress?.defaultAddress ?: "" + // networkId = networkId, + // fromTokenAddress = fromTokenAddress, + // toTokenAddress = toTokenAddress, + // amount = amount.toStringWithRightOffset(), + // slippage = DEFAULT_SLIPPAGE, + // fromWalletAddress = getWalletAddress(networkId), ).let { val swapData = it.dataModel if (swapData != null) { @@ -532,9 +540,9 @@ internal class SwapInteractorImpl @Inject constructor( networkId = networkId, amountToSend = amount.value, currencyToSend = swapCurrencyConverter.convert(fromToken.currency), - destinationAddress = swapData.transaction.toWalletAddress, + destinationAddress = swapData.transaction.txTo, increaseBy = INCREASE_GAS_LIMIT_BY, - data = swapData.transaction.data, + data = (swapData.transaction as ExpressTransactionModel.DEX).txData, derivationPath = derivationPath, ) val txFeeState = proxyFeesToFeeState(networkId, feeData) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 31863dc852..d919661845 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -32,16 +32,6 @@ interface SwapRepository { */ suspend fun addressForTrust(networkId: String): String - @Suppress("LongParameterList") - suspend fun prepareSwapTransaction( - networkId: String, - fromTokenAddress: String, - toTokenAddress: String, - amount: String, - fromWalletAddress: String, - slippage: Int, - ): AggregatedSwapDataModel - /** * Returns a tangem fee for swap in percents * Example: 0.35% @@ -67,7 +57,7 @@ interface SwapRepository { ): String @Suppress("LongParameterList") - suspend fun getExchangeQuote( + suspend fun getExchangeData( fromContractAddress: String, fromNetwork: String, toContractAddress: String, @@ -75,7 +65,8 @@ interface SwapRepository { fromAmount: String, providerId: String, rateType: RateType, - ): ExchangeQuote + toAddress: String, + ): AggregatedSwapDataModel fun getNativeTokenForNetwork(networkId: String): CryptoCurrency } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index fbcbeb51b0..a00be2a2bc 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -1,20 +1,10 @@ package com.tangem.feature.swap.domain.models sealed class DataError { - object NoError : DataError() - data class UnknownError(val message: String) : DataError() - object InsufficientLiquidity : DataError() + object UnknownError : DataError() + data class Error(val message: String) : DataError() } fun mapErrors(error: String?): DataError { - return if (error == null) { - DataError.NoError - } else { - when (error) { - INSUFFICIENT_LIQUIDITY_ERROR -> DataError.InsufficientLiquidity - else -> DataError.UnknownError(error) - } - } -} - -private const val INSUFFICIENT_LIQUIDITY_ERROR = "insufficient liquidity" \ No newline at end of file + return error?.let { DataError.Error(it) } ?: DataError.UnknownError +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt index 317b0da94f..45a895181e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt @@ -11,5 +11,5 @@ import com.tangem.feature.swap.domain.models.DataError */ data class AggregatedSwapDataModel( val dataModel: T?, - val error: DataError = DataError.NoError, + val error: DataError = DataError.UnknownError, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt new file mode 100644 index 0000000000..30271929e7 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt @@ -0,0 +1,30 @@ +package com.tangem.feature.swap.domain.models.domain + +import com.tangem.feature.swap.domain.models.SwapAmount + +sealed class ExpressTransactionModel { + + abstract val fromAmount: SwapAmount + abstract val toAmount: SwapAmount + abstract val txId: String + abstract val txTo: String + + data class DEX( + override val fromAmount: SwapAmount, + override val toAmount: SwapAmount, + override val txId: String, + override val txTo: String, + val txFrom: String, + val txData: String, + ): ExpressTransactionModel() + + data class CEX( + override val fromAmount: SwapAmount, + override val toAmount: SwapAmount, + override val txId: String, + override val txTo: String, + val externalTxId: String, + val externalTxUrl: String, + ): ExpressTransactionModel() + +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt index 7c15701329..8b2927968f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt @@ -2,13 +2,7 @@ package com.tangem.feature.swap.domain.models.domain import com.tangem.feature.swap.domain.models.SwapAmount -/** - * Swap transaction model - * - * @property toTokenAmount amount "target" token - * @property transaction info about transaction - */ data class SwapDataModel( val toTokenAmount: SwapAmount, - val transaction: TransactionModel, + val transaction: ExpressTransactionModel, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index e7623069ab..6d740e3fa6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -574,7 +574,7 @@ internal class StateBuilder( // todo use if needed later // DataError.InsufficientLiquidity -> TODO() // DataError.NoError -> TODO() - is DataError.UnknownError -> addWarning(uiState, error.message, true, onClick) + is DataError.Error -> addWarning(uiState, error.message, true, onClick) else -> addWarning(uiState, null, false) {} } } diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index f8a413d31d..248cc20bd1 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -1,12 +1,10 @@ plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.jvm) id("configuration") } dependencies { + /** Coroutines */ implementation(deps.kotlin.coroutines) - implementation(projects.domain.tokens.models) - } \ No newline at end of file From 51540c732a5a9e40c158160ad5d047d504e5bef1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Nov 2023 15:44:02 +0400 Subject: [PATCH 046/139] Updated on 2026-08-14 --- .../components/SettingsSwitchItem.kt | 1 + .../com/tangem/core/ui/components/Cards.kt | 22 +- .../tangem/core/ui/components/TangemSwitch.kt | 2 +- .../com/tangem/core/ui/components/Warnings.kt | 41 +++- .../core/ui/decorations/RoundedDecorations.kt | 32 ++- .../common/state/ChooseWalletState.kt | 31 +++ .../common/state/NetworkItemState.kt | 90 ++++++++ .../presentation/common/state/WalletState.kt | 8 + .../ChooseWalletStatePreviewData.kt | 27 +++ .../common/ui/ChooseWalletScreen.kt | 151 +++++++++++++ .../common/ui/components/NetworkItem.kt | 157 ++++++++++++++ .../ui/components/SimpleSelectionBlock.kt | 70 ++++++ .../managetokens/state/ChooseNetworkState.kt | 11 + .../state/DerivationNotificationState.kt | 7 +- .../managetokens/state/TokenItemState.kt | 10 +- .../ChooseNetworkStatePreviewData.kt | 60 +++++ .../DerivationNotificationStatePreviewData.kt | 5 +- .../previewdata/TokenItemStatePreviewData.kt | 13 +- .../managetokens/ui/ChooseNetworkScreen.kt | 205 ++++++++++++++++++ .../ui/components/TokenRowItem.kt | 6 +- 20 files changed, 915 insertions(+), 34 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index 74815a675f..b584136754 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -63,6 +63,7 @@ internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier checked = item.isChecked, enabled = item.isEnabled, onCheckedChange = item.onCheckedChange, + checkedColor = TangemTheme.colors.icon.accent, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt index 847c27307f..91dd9a2506 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt @@ -13,6 +13,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -162,11 +163,12 @@ fun CardWithIcon( * >Figma component */ @Composable -fun IconWithTitleAndDescription( +internal fun IconWithTitleAndDescription( title: String, - description: String, + description: String?, icon: @Composable () -> Unit, additionalContent: @Composable () -> Unit = {}, + iconBackground: Color = TangemTheme.colors.background.secondary, ) { Row( modifier = Modifier @@ -182,7 +184,7 @@ fun IconWithTitleAndDescription( Box( modifier = Modifier .background( - color = TangemTheme.colors.background.secondary, + color = iconBackground, shape = CircleShape, ) .height(TangemTheme.dimens.size40) @@ -205,12 +207,14 @@ fun IconWithTitleAndDescription( color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle1, ) - SpacerH4() - Text( - text = description, - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - ) + if (description != null) { + SpacerH4() + Text( + text = description, + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + ) + } } additionalContent() diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt index c4d41ef91b..8e0081a633 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt @@ -23,7 +23,7 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun TangemSwitch( onCheckedChange: (Boolean) -> Unit, - checkedColor: Color = TangemTheme.colors.icon.accent, + checkedColor: Color = TangemTheme.colors.control.checked, uncheckedColor: Color = TangemTheme.colors.icon.informative, size: Dp = 48.dp, checked: Boolean = false, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt index b6b858d2cd..d0d3315f20 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt @@ -9,6 +9,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R @@ -36,6 +37,28 @@ fun WarningCard(title: String, description: String, icon: @Composable (() -> Uni ) } +/** + * A card with a warning icon to the left and title without description shown to the right of it. + * + * @param title title of the warning in bold + * + * @see Figma component + */ +@Composable +fun WarningCardTitleOnly(title: String, icon: @Composable (() -> Unit)? = null) { + WarningCardMaterial3Style( + content = { + WarningBody( + title = title, + description = null, + icon = icon, + iconBackground = TangemTheme.colors.button.disabled, + ) + }, + ) +} + /** * [WarningCard], but clickable (with an 'greater then' icon to the left) * @@ -105,7 +128,8 @@ fun RefreshableWaringCard( @Composable private fun WarningBody( title: String, - description: String, + description: String?, + iconBackground: Color = TangemTheme.colors.background.secondary, icon: @Composable (() -> Unit)? = null, additionalContent: @Composable () -> Unit = {}, ) { @@ -119,6 +143,7 @@ private fun WarningBody( contentDescription = null, ) }, + iconBackground = iconBackground, ) } @@ -136,6 +161,20 @@ private fun WarningCardSurface(onClick: (() -> Unit)? = null, content: @Composab } } +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun WarningCardMaterial3Style(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) { + Card( + shape = RoundedCornerShape(TangemTheme.dimens.radius16), + backgroundColor = TangemTheme.colors.button.disabled, + elevation = TangemTheme.dimens.elevation0, + onClick = onClick ?: {}, + enabled = onClick != null, + ) { + content() + } +} + // endregion elements // region Preview diff --git a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt index 362f4d95e9..4a014cd824 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt @@ -7,18 +7,34 @@ import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import com.tangem.core.ui.res.TangemTheme -fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modifier = composed { - val modifierWithHorizontalPadding = this.padding(horizontal = TangemTheme.dimens.spacing16) +fun Modifier.roundedShapeItemDecoration( + currentIndex: Int, + lastIndex: Int, + addDefaultPadding: Boolean = true, +): Modifier = composed { + val modifier = if (addDefaultPadding) this.padding(horizontal = TangemTheme.dimens.spacing16) else this val isSingleItem = currentIndex == 0 && lastIndex == 0 when { isSingleItem -> { - modifierWithHorizontalPadding - .padding(top = TangemTheme.dimens.spacing14) + modifier + .then( + if (addDefaultPadding) { + Modifier.padding(top = TangemTheme.dimens.spacing14) + } else { + Modifier + }, + ) .clip(shape = TangemTheme.shapes.roundedCornersXMedium) } currentIndex == 0 -> { - modifierWithHorizontalPadding - .padding(top = TangemTheme.dimens.spacing14) + modifier + .then( + if (addDefaultPadding) { + Modifier.padding(top = TangemTheme.dimens.spacing14) + } else { + Modifier + }, + ) .clip( shape = RoundedCornerShape( topStart = TangemTheme.dimens.radius16, @@ -27,7 +43,7 @@ fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modi ) } currentIndex == lastIndex -> { - modifierWithHorizontalPadding + modifier .clip( shape = RoundedCornerShape( bottomStart = TangemTheme.dimens.radius16, @@ -35,6 +51,6 @@ fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modi ), ) } - else -> modifierWithHorizontalPadding + else -> modifier } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt new file mode 100644 index 0000000000..44a0529872 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt @@ -0,0 +1,31 @@ +package com.tangem.managetokens.presentation.common.state + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.managetokens.impl.R +import kotlinx.collections.immutable.ImmutableList + +internal sealed class ChooseWalletState { + data class Choose( + val wallets: ImmutableList, + val selectedWallet: WalletState?, + val onChooseWalletClick: () -> Unit, + val onCloseChoosingWalletClick: () -> Unit, + ) : ChooseWalletState() + + object NoSelection : ChooseWalletState() + + class Warning(val type: ChooseWalletWarning) : ChooseWalletState() { + val message: TextReference + get() = when (type) { + ChooseWalletWarning.SINGLE_CURRENCY -> + TextReference.Res(R.string.manage_tokens_wallet_support_only_one_network_title) + ChooseWalletWarning.WALLET_INCOMPATIBLE -> + TextReference.Res(R.string.manage_tokens_wallet_does_not_supported_blockchain) + } + } +} + +enum class ChooseWalletWarning { + SINGLE_CURRENCY, + WALLET_INCOMPATIBLE, +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt new file mode 100644 index 0000000000..a759836afc --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt @@ -0,0 +1,90 @@ +package com.tangem.managetokens.presentation.common.state + +import androidx.compose.runtime.MutableState +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.getGreyedOutIconRes +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState + +/** + * Network item state + * + * @property name network name + * @property protocolName network protocol name + * @property id network id + * @property blockchain blockchain + * @property iconRes network icon id from resources + */ +internal sealed interface NetworkItemState { + + val name: String + val protocolName: String + val id: String + val blockchain: Blockchain + + val iconRes: Int + get() = when (this) { + is Selectable -> this.iconResId + is Toggleable -> this.iconResId.value + } + + /** + * Network item state that can be added and deleted + * + * @property name network name + * @property protocolName network protocol name + * @property id network id + * @property blockchain blockchain + * @property iconResId network icon id from resources + * @property isMainNetwork flag that determines if the network is the main network for the token + * @property isAdded flag that determines if the user has saved the network + * @property address contract address + * @property decimals decimal count + * @property onToggleClick lambda be invoked when switch is been toggled + */ + @Suppress("LongParameterList") + class Toggleable( + override val name: String, + override val protocolName: String, + override val id: String, + override val blockchain: Blockchain, + val iconResId: MutableState, + val isMainNetwork: Boolean, + val isAdded: MutableState, + val address: String?, + val decimals: Int?, + val onToggleClick: (TokenItemState.Loaded, Toggleable) -> Unit, + ) : NetworkItemState { + + /** + * Change toggle state [isAdded]. + * + * It is a hack that helps us to change element of flow + */ + fun changeToggleState() { + val reverseState = !isAdded.value + isAdded.value = reverseState + iconResId.value = if (reverseState) getActiveIconRes(blockchain.id) else getGreyedOutIconRes(blockchain.id) + } + } + + /** + * Network item state that can be selected + * + * @property name network name + * @property protocolName network protocol name + * @property iconResId network icon id from resources + * @property id network id + * @property blockchain blockchain + * @property onNetworkClick lambda be invoked when network item is been clicked + * + */ + class Selectable( + override val name: String, + override val protocolName: String, + val iconResId: Int, + override val id: String, + override val blockchain: Blockchain, + val onNetworkClick: (NetworkItemState) -> Unit, + ) : NetworkItemState +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt new file mode 100644 index 0000000000..6af48736a4 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt @@ -0,0 +1,8 @@ +package com.tangem.managetokens.presentation.common.state + +internal data class WalletState( + val walletId: String, + val artworkUrl: String?, + val walletName: String, + val onSelected: (String) -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt new file mode 100644 index 0000000000..f231654b96 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt @@ -0,0 +1,27 @@ +package com.tangem.managetokens.presentation.common.state.previewdata + +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.state.WalletState +import kotlinx.collections.immutable.persistentListOf + +internal object ChooseWalletStatePreviewData { + + val state: ChooseWalletState.Choose + get() = ChooseWalletState.Choose( + wallets = persistentListOf( + walletState, + walletState.copy(walletId = "2"), + ), + selectedWallet = walletState, + onChooseWalletClick = {}, + onCloseChoosingWalletClick = {}, + ) + + private val walletState: WalletState + get() = WalletState( + walletName = "My wallet", + walletId = "1", + artworkUrl = "", + onSelected = {}, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt new file mode 100644 index 0000000000..d0dab333d8 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt @@ -0,0 +1,151 @@ +package com.tangem.managetokens.presentation.common.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.state.WalletState +import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData + +@Composable +internal fun ChooseWalletScreen(state: ChooseWalletState.Choose, modifier: Modifier = Modifier) { + LazyColumn( + modifier = modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) { + item { + Box( + modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size44), + ) { + IconButton( + onClick = state.onCloseChoosingWalletClick, + modifier = Modifier.align(Alignment.CenterStart), + ) { + Icon( + painterResource(id = R.drawable.ic_back_24), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + } + Text( + text = stringResource(id = R.string.manage_tokens_wallet_selector_title), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center), + ) + } + } + items( + count = state.wallets.count(), + key = { index -> state.wallets[index].walletId }, + ) { index -> + WalletItem( + wallet = state.wallets[index], + selectedWallet = state.selectedWallet, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = state.wallets.lastIndex, + addDefaultPadding = false, + ), + ) + } + item { + SpacerH(height = TangemTheme.dimens.spacing16) + } + } +} + +@Composable +private fun WalletItem(wallet: WalletState, selectedWallet: WalletState?, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .clickable { wallet.onSelected(wallet.walletId) } + .background(TangemTheme.colors.background.action) + .defaultMinSize(minHeight = TangemTheme.dimens.size72) + .padding(horizontal = TangemTheme.dimens.spacing16), + verticalAlignment = Alignment.CenterVertically, + ) { + SubcomposeAsyncImage( + modifier = Modifier.size(height = TangemTheme.dimens.size30, width = TangemTheme.dimens.size50), + + model = ImageRequest.Builder(context = LocalContext.current) + .data(wallet.artworkUrl) + .crossfade(enable = true) + .build(), + loading = { + Image( + painter = painterResource(R.drawable.card_placeholder_primary), + contentDescription = null, + ) + }, + error = { + Image( + painter = painterResource(R.drawable.card_placeholder_primary), + contentDescription = null, + ) + }, + contentDescription = null, + ) + SpacerW12() + Text( + text = wallet.walletName, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + SpacerWMax() + if (selectedWallet == wallet) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + } + } +} + +@Preview +@Composable +private fun Preview_ChooseWalletScreen_Light() { + TangemTheme(isDark = false) { + ChooseWalletScreen( + state = ChooseWalletStatePreviewData.state, + ) + } +} + +@Preview +@Composable +private fun Preview_ChooseWalletScreen_Dark() { + TangemTheme(isDark = false) { + ChooseWalletScreen( + state = ChooseWalletStatePreviewData.state, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt new file mode 100644 index 0000000000..18de0a171a --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt @@ -0,0 +1,157 @@ +package com.tangem.managetokens.presentation.common.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.NetworkItemState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData + +@Composable +internal fun NetworkItem(state: NetworkItemState, tokenState: TokenItemState.Loaded?, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background(TangemTheme.colors.background.action) + .defaultMinSize(minHeight = TangemTheme.dimens.size68) + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + NetworkIcon(model = state) + SpacerW(width = TangemTheme.dimens.spacing12) + Text( + text = state.name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + ) + SpacerW(width = TangemTheme.dimens.spacing6) + Text( + text = state.protocolName, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + modifier = Modifier + .weight(1f), + ) + if (state is NetworkItemState.Toggleable) { + TangemSwitch( + onCheckedChange = { + state.onToggleClick(tokenState!!, state) + }, + checked = state.isAdded.value, + ) + } + } +} + +@Composable +internal fun NetworkIcon(model: NetworkItemState, modifier: Modifier = Modifier) { + Box(modifier = modifier.size(size = TangemTheme.dimens.size36)) { + val isAdded = when (model) { + is NetworkItemState.Selectable -> true + is NetworkItemState.Toggleable -> model.isAdded.value + } + + if (!isAdded) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(CircleShape) + .background(TangemTheme.colors.control.unchecked), + ) + } + Icon( + painter = painterResource(id = model.iconRes), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size36), + tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary, + ) + + if (model is NetworkItemState.Toggleable && model.isMainNetwork) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(TangemTheme.dimens.size10) + .clip(CircleShape) + .background(TangemTheme.colors.stroke.transparency), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size8) + .clip(CircleShape) + .background(TangemTheme.colors.icon.accent), + ) + } + } + } +} + +@Preview +@Composable +private fun Preview_NetworkItem_Light(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { + TangemTheme(isDark = false) { + NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) + } +} + +@Preview +@Composable +private fun Preview_NetworkItem_Dark(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { + TangemTheme(isDark = true) { + NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) + } +} + +private class NetworkItemStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + NetworkItemState.Toggleable( + name = "Ethereum", + protocolName = "ETH", + iconResId = mutableStateOf(R.drawable.img_polygon_22), + isMainNetwork = true, + isAdded = mutableStateOf(true), + id = "", + address = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.Ethereum, + decimals = 0, + ), + NetworkItemState.Toggleable( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = mutableStateOf(R.drawable.ic_bsc_16), + isMainNetwork = false, + isAdded = mutableStateOf(false), + id = "", + address = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.BSC, + decimals = 0, + ), + NetworkItemState.Selectable( + name = "Ethereum", + protocolName = "ETH", + iconResId = R.drawable.img_polygon_22, + id = "", + onNetworkClick = { }, + blockchain = Blockchain.Ethereum, + ), + ), +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt new file mode 100644 index 0000000000..c136cb1e28 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt @@ -0,0 +1,70 @@ +package com.tangem.managetokens.presentation.common.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun SimpleSelectionBlock( + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + roundedCorners: Boolean = true, +) { + Column( + modifier = modifier + .then( + if (roundedCorners) { + Modifier.clip(shape = RoundedCornerShape(TangemTheme.dimens.radius16)) + } else { + Modifier + }, + ) + .background(color = TangemTheme.colors.background.action) + .clickable { onClick() } + .padding( + horizontal = TangemTheme.dimens.spacing20, + vertical = TangemTheme.dimens.spacing16, + ) + .fillMaxWidth(), + ) { + Text( + text = title, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + ) + SpacerH(height = TangemTheme.dimens.spacing4) + Text( + text = subtitle, + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + ) + } +} + +@Preview +@Composable +private fun Preview_SimpleSelectionBlock_Light() { + TangemTheme(isDark = false) { + SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) + } +} + +@Preview +@Composable +private fun Preview_SimpleSelectionBlock_Dark() { + TangemTheme(isDark = true) { + SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt new file mode 100644 index 0000000000..df2a7f015f --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt @@ -0,0 +1,11 @@ +package com.tangem.managetokens.presentation.managetokens.state + +import com.tangem.managetokens.presentation.common.state.NetworkItemState +import kotlinx.collections.immutable.ImmutableList + +internal data class ChooseNetworkState( + val nativeNetworks: ImmutableList, + val nonNativeNetworks: ImmutableList, + val onNonNativeNetworkHintClick: () -> Unit, + val onCloseChooseNetworkScreen: () -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt index 8856ab58c5..0cfdfa16d1 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt @@ -8,7 +8,8 @@ import com.tangem.features.managetokens.impl.R data class DerivationNotificationState( val totalNeeded: Int, - val missingAddressesCount: Int, + val totalWallets: Int, + val walletsToDerive: Int, val onGenerateClick: () -> Unit, ) { val config = NotificationConfig( @@ -25,8 +26,8 @@ data class DerivationNotificationState( onClick = onGenerateClick, additionalText = pluralReference( id = R.plurals.manage_tokens_number_of_wallets_android, - count = totalNeeded, - formatArgs = wrappedList(missingAddressesCount, totalNeeded), + count = totalWallets, + formatArgs = wrappedList(walletsToDerive, totalWallets), ), ), ) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt index ae386cc7c1..7262bbe337 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt @@ -1,5 +1,7 @@ package com.tangem.managetokens.presentation.managetokens.state +import androidx.compose.runtime.MutableState + internal sealed class TokenItemState { abstract val id: String @@ -9,11 +11,13 @@ internal sealed class TokenItemState { data class Loaded( override val id: String, val name: String, - val currencyId: String, + val currencySymbol: String, + val tokenId: String, val tokenIcon: TokenIconState, val quotes: QuotesState, val rate: String?, - val availableAction: TokenButtonType, - val onButtonClick: (String) -> Unit, + val availableAction: MutableState, + val chooseNetworkState: ChooseNetworkState, + val onButtonClick: (Loaded) -> Unit, ) : TokenItemState() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt new file mode 100644 index 0000000000..12be7db588 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt @@ -0,0 +1,60 @@ +package com.tangem.managetokens.presentation.managetokens.state.previewdata + +import androidx.compose.runtime.mutableStateOf +import com.tangem.blockchain.common.Blockchain +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.NetworkItemState +import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState +import kotlinx.collections.immutable.toImmutableList + +internal object ChooseNetworkStatePreviewData { + + val state = ChooseNetworkState( + nativeNetworks = nativeNetworks.toImmutableList(), + nonNativeNetworks = nonNativeNetworks.toImmutableList(), + onNonNativeNetworkHintClick = {}, + onCloseChooseNetworkScreen = {}, + ) +} + +internal val nativeNetworks = listOf( + NetworkItemState.Toggleable( + name = "Ethereum", + protocolName = "ETH", + iconResId = mutableStateOf(R.drawable.img_polygon_22), + isMainNetwork = true, + isAdded = mutableStateOf(true), + id = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.Ethereum, + address = "", + decimals = 0, + ), +) + +internal val nonNativeNetworks = listOf( + NetworkItemState.Toggleable( + name = "Ethereum", + protocolName = "ETH", + iconResId = mutableStateOf(R.drawable.img_kusama_22), + isMainNetwork = false, + isAdded = mutableStateOf(true), + id = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.Ethereum, + address = "", + decimals = 0, + ), + NetworkItemState.Toggleable( + name = "BNB SMART CHAIN", + protocolName = "BEP20", + iconResId = mutableStateOf(R.drawable.ic_bsc_16), + isMainNetwork = false, + isAdded = mutableStateOf(false), + id = "", + onToggleClick = { _, _ -> }, + blockchain = Blockchain.BSC, + address = "", + decimals = 0, + ), +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt index 207ea75cb8..b5d88b814a 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt @@ -4,8 +4,9 @@ import com.tangem.managetokens.presentation.managetokens.state.DerivationNotific object DerivationNotificationStatePreviewData { val state = DerivationNotificationState( - totalNeeded = 3, - missingAddressesCount = 2, + totalNeeded = 5, + totalWallets = 3, + walletsToDerive = 2, onGenerateClick = {}, ) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt index bdfedacd3e..01ed9bdda9 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.state.previewdata +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.graphics.Color import com.tangem.managetokens.presentation.managetokens.state.* import kotlinx.collections.immutable.persistentListOf @@ -13,7 +14,8 @@ internal object TokenItemStatePreviewData { get() = TokenItemState.Loaded( id = "BTC", name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", - currencyId = "BTC", + tokenId = "BTC", + currencySymbol = "BTC", tokenIcon = tokenIconState, quotes = QuotesState.Content( priceChange = "0.43%", @@ -21,15 +23,17 @@ internal object TokenItemStatePreviewData { chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 4f), ), rate = "31 285.72$", - availableAction = TokenButtonType.ADD, + availableAction = mutableStateOf(TokenButtonType.ADD), onButtonClick = {}, + chooseNetworkState = ChooseNetworkStatePreviewData.state, ) val loadedPriceUp: TokenItemState get() = TokenItemState.Loaded( id = "BTC", name = "Bitcoin", - currencyId = "BTC", + tokenId = "BTC", + currencySymbol = "BTC", tokenIcon = tokenIconState, quotes = QuotesState.Content( priceChange = "0.43%", @@ -37,8 +41,9 @@ internal object TokenItemStatePreviewData { chartData = persistentListOf(1f, 3f, 4f, 8f, 12f, 10f, 8f, 3f, 5f, 7f), ), rate = "31 285.72$", - availableAction = TokenButtonType.NOT_AVAILABLE, + availableAction = mutableStateOf(TokenButtonType.NOT_AVAILABLE), onButtonClick = {}, + chooseNetworkState = ChooseNetworkStatePreviewData.state, ) private val tokenIconState: TokenIconState diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt new file mode 100644 index 0000000000..ebc600073f --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt @@ -0,0 +1,205 @@ +package com.tangem.managetokens.presentation.managetokens.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.WarningCardTitleOnly +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData +import com.tangem.managetokens.presentation.common.ui.components.NetworkItem +import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock +import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData + +@Composable +internal fun ChooseNetworkScreen( + state: TokenItemState.Loaded, + walletState: ChooseWalletState, + modifier: Modifier = Modifier, +) { + val networkState = state.chooseNetworkState + LazyColumn( + modifier = modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) { + item { + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_title), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth(), + ) + } + + when (walletState) { + is ChooseWalletState.Choose -> { + item { + SpacerH(height = TangemTheme.dimens.spacing10) + } + item { + SimpleSelectionBlock( + title = stringResource(id = R.string.manage_tokens_network_selector_wallet), + subtitle = walletState.selectedWallet?.walletName ?: "", + onClick = walletState.onChooseWalletClick, + ) + } + } + ChooseWalletState.NoSelection -> Unit + is ChooseWalletState.Warning -> { + item { + SpacerH(height = TangemTheme.dimens.spacing10) + } + item { + WarningCardTitleOnly( + title = stringResource(id = R.string.manage_tokens_wallet_support_only_one_network_title), + ) + } + } + } + + item { + SpacerH(height = TangemTheme.dimens.spacing16) + } + + item { + if (networkState.nativeNetworks.isNotEmpty()) { + NativeNetworks(networkState = networkState, tokenState = state) + } + } + + if (networkState.nonNativeNetworks.isNotEmpty()) { + item { + NonNativeNetworksHeader(networkState.onNonNativeNetworkHintClick) + } + item { + SpacerH(height = TangemTheme.dimens.spacing8) + } + item { + this@LazyColumn.NonNativeNetworks(networkState = networkState, tokenState = state) + } + } + } +} + +@Composable +private fun NativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) { + Column { + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_native_title), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + ) + SpacerH(height = TangemTheme.dimens.spacing2) + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_native_subtitle), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + SpacerH(height = TangemTheme.dimens.spacing8) + + networkState.nativeNetworks.forEachIndexed { index, network -> + NetworkItem( + state = network, + tokenState = tokenState, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = networkState.nativeNetworks.lastIndex, + addDefaultPadding = false, + ), + ) + } + SpacerH(height = TangemTheme.dimens.spacing16) + } +} + +@Composable +private fun LazyListScope.NonNativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) { + items( + count = networkState.nonNativeNetworks.count(), + key = { index -> networkState.nonNativeNetworks[index].id }, + ) { index -> + NetworkItem( + state = networkState.nonNativeNetworks[index], + tokenState = tokenState, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = networkState.nonNativeNetworks.lastIndex, + addDefaultPadding = false, + ), + ) + } + item { + SpacerH(height = TangemTheme.dimens.spacing16) + } +} + +@Composable +private fun NonNativeNetworksHeader(onNonNativeNetworkHintClick: () -> Unit) { + Column { + Row { + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_non_native_title), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + ) + SpacerW(width = TangemTheme.dimens.spacing2) + Icon( + painter = painterResource(id = R.drawable.ic_information_24), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .clickable { onNonNativeNetworkHintClick() }, + ) + } + SpacerH(height = TangemTheme.dimens.spacing2) + Text( + text = stringResource(id = R.string.manage_tokens_network_selector_non_native_subtitle), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } +} + +@Preview +@Composable +private fun Preview_ChooseNetworkScreen_Light() { + TangemTheme(isDark = false) { + ChooseNetworkScreen( + state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, + walletState = ChooseWalletStatePreviewData.state, + ) + } +} + +@Preview +@Composable +private fun Preview_ChooseNetworkScreen_Dark() { + TangemTheme(isDark = true) { + ChooseNetworkScreen( + state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, + walletState = ChooseWalletStatePreviewData.state, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt index 139e4f9652..12a403e3e4 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt @@ -54,7 +54,7 @@ private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = M modifier = Modifier .weight(weight = 1f), ) { - TokenName(name = state.name, currencyId = state.currencyId) + TokenName(name = state.name, currencyId = state.currencySymbol) TokenPriceData(price = state.rate, quotesState = state.quotes) } SpacerW24() @@ -67,8 +67,8 @@ private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = M } TokenButton( - type = state.availableAction, - onClick = { state.onButtonClick(state.currencyId) }, + type = state.availableAction.value, + onClick = { state.onButtonClick(state) }, ) } } From 00cc9ea226290627889ed8a9a0deaafbf6a03cd2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Nov 2023 16:06:34 +0200 Subject: [PATCH 047/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 175 ++++++++++++------ 1 file changed, 119 insertions(+), 56 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 67be5cd7a5..bda5b8c320 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -228,46 +228,114 @@ internal class SwapInteractorImpl @Inject constructor( amountToSwap: String, selectedFee: FeeType, ): Map { - syncWalletBalanceForTokens(networkId, listOf(fromToken.currency, toToken.currency)) - val amountDecimal = toBigDecimalOrNull(amountToSwap) - if (amountDecimal == null || amountDecimal.signum() == 0) { - return providers.associateWith { createEmptyAmountState(networkId, fromToken.currency, toToken.currency) } - } - val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) - val fromTokenAddress = getTokenAddress(fromToken.currency) - val toTokenAddress = getTokenAddress(toToken.currency) - val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount) - if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { - allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) - transactionManager.updateWalletManager(networkId, derivationPath) - } - val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null) - return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { - // TODO - providers.associateWith { - loadSwapData( - networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, - fromToken = fromToken, - toToken = toToken, - amount = amount, - selectedFee = selectedFee, - ) + return providers.map { provider -> + syncWalletBalanceForTokens(networkId, listOf(fromToken.currency, toToken.currency)) + val amountDecimal = toBigDecimalOrNull(amountToSwap) + if (amountDecimal == null || amountDecimal.signum() == 0) { + return providers.associateWith { + createEmptyAmountState( + networkId, + fromToken.currency, + toToken.currency + ) + } } + val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) + val fromTokenAddress = getTokenAddress(fromToken.currency) + val toTokenAddress = getTokenAddress(toToken.currency) + val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount) + if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { + allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) + transactionManager.updateWalletManager(networkId, derivationPath) + } + val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null) + + when (provider.type) { + ExchangeProviderType.DEX -> { + manageDex( + networkId = networkId, + fromToken = fromToken, + toToken = toToken, + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + provider = provider, + selectedFee = selectedFee, + amount = amount, + isAllowedToSpend = isAllowedToSpend, + isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + ) + } + ExchangeProviderType.CEX -> { + manageCex( + networkId = networkId, + fromToken = fromToken, + toToken = toToken, + provider = provider, + amount = amount, + isAllowedToSpend = isAllowedToSpend, + isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + ) + } + } + }.toMap() + } + + private suspend fun manageDex( + networkId: String, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, + fromTokenAddress: String, + toTokenAddress: String, + provider: SwapProvider, + selectedFee: FeeType, + amount: SwapAmount, + isAllowedToSpend: Boolean, + isBalanceWithoutFeeEnough: Boolean, + ): Pair { + return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { + provider to loadSwapData( + provider = provider, + networkId = networkId, + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + fromToken = fromToken, + toToken = toToken, + amount = amount, + selectedFee = selectedFee, + ) } else { - loadQuoteData( + provider to loadQuoteData( networkId = networkId, amount = amount, fromTokenStatus = fromToken, toTokenStatus = toToken, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - providers = providers, + provider = provider, ) } } + private suspend fun manageCex( + networkId: String, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, + provider: SwapProvider, + amount: SwapAmount, + isAllowedToSpend: Boolean, + isBalanceWithoutFeeEnough: Boolean, + ): Pair { + return provider to loadQuoteData( + networkId = networkId, + amount = amount, + fromTokenStatus = fromToken, + toTokenStatus = toToken, + isAllowedToSpend = isAllowedToSpend, + isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + provider = provider, + ) + } + @Deprecated("used in old swap mechanism") override suspend fun onSwap( networkId: String, @@ -422,39 +490,33 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, fromTokenStatus: CryptoCurrencyStatus, toTokenStatus: CryptoCurrencyStatus, - providers: List, + provider: SwapProvider, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, - ): Map { + ): SwapState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency return coroutineScope { - val quoteRequests = providers.map { provider -> - async { - provider to repository.findBestQuote( - fromContractAddress = fromToken.getContractAddress(), - fromNetwork = fromToken.network.backendId, - toContractAddress = toToken.getContractAddress(), - toNetwork = toToken.network.backendId, - fromAmount = amount.toStringWithRightOffset(), - providerId = provider.providerId, - rateType = RateType.FLOAT, - ) - } - } + val quotes = repository.findBestQuote( + fromContractAddress = fromToken.getContractAddress(), + fromNetwork = fromToken.network.backendId, + toContractAddress = toToken.getContractAddress(), + toNetwork = toToken.network.backendId, + fromAmount = amount.toStringWithRightOffset(), + providerId = provider.providerId, + rateType = RateType.FLOAT, + ) - quoteRequests.awaitAll().map { - it.first to - getState( - quoteDataModel = it.second, - amount = amount, - fromToken = fromTokenStatus, - toToken = toTokenStatus, - networkId = networkId, - isAllowedToSpend = isAllowedToSpend, - isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - ) - }.associate { it.first to it.second } + + getState( + quoteDataModel = quotes, + amount = amount, + fromToken = fromTokenStatus, + toToken = toTokenStatus, + networkId = networkId, + isAllowedToSpend = isAllowedToSpend, + isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + ) } } @@ -510,6 +572,7 @@ internal class SwapInteractorImpl @Inject constructor( */ @Suppress("LongParameterList") private suspend fun loadSwapData( + provider: SwapProvider, networkId: String, fromTokenAddress: String, toTokenAddress: String, @@ -524,7 +587,7 @@ internal class SwapInteractorImpl @Inject constructor( toContractAddress = toToken.currency.getContractAddress(), toNetwork = toToken.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), - providerId = "1", //TODO + providerId = provider.providerId, rateType = RateType.FLOAT, toAddress = toToken.value.networkAddress?.defaultAddress ?: "" // networkId = networkId, From ca89626d270052ead9c3b6f99d67147d8fd3e7ec Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Nov 2023 16:25:24 +0200 Subject: [PATCH 048/139] Updated on 2026-08-14 --- .../tangem/datasource/api/express/TangemExpressApi.kt | 1 + .../java/com/tangem/feature/swap/SwapRepositoryImpl.kt | 4 +++- .../com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 9 ++------- .../com/tangem/feature/swap/domain/SwapRepository.kt | 1 + 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 6144d6ad9e..8fff666e26 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -46,6 +46,7 @@ interface TangemExpressApi { @Query("providerId") providerId: String, @Query("rateType") rateType: String, @Query("toAddress") toAddress: String, + @Query("refundAddress") refundAddress: String, ): ApiResponse @GET("exchange-result") diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 3571784929..b143fcf4d4 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -185,6 +185,7 @@ internal class SwapRepositoryImpl @Inject constructor( providerId: String, rateType: RateType, toAddress: String, + refundAddress: String ): AggregatedSwapDataModel { return withContext(coroutineDispatcher.io) { try { @@ -196,7 +197,8 @@ internal class SwapRepositoryImpl @Inject constructor( fromAmount = fromAmount, providerId = providerId, rateType = rateType.name.lowercase(), - toAddress = toAddress + toAddress = toAddress, + refundAddress = refundAddress ).getOrThrow() AggregatedSwapDataModel( dataModel = expressDataConverter.convert(response) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index bda5b8c320..be2096f0c4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -589,13 +589,8 @@ internal class SwapInteractorImpl @Inject constructor( fromAmount = amount.toStringWithRightOffset(), providerId = provider.providerId, rateType = RateType.FLOAT, - toAddress = toToken.value.networkAddress?.defaultAddress ?: "" - // networkId = networkId, - // fromTokenAddress = fromTokenAddress, - // toTokenAddress = toTokenAddress, - // amount = amount.toStringWithRightOffset(), - // slippage = DEFAULT_SLIPPAGE, - // fromWalletAddress = getWalletAddress(networkId), + toAddress = toToken.value.networkAddress?.defaultAddress ?: "", + refundAddress = fromToken.value.networkAddress?.defaultAddress ?: "", ).let { val swapData = it.dataModel if (swapData != null) { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index d919661845..86d54285e5 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -66,6 +66,7 @@ interface SwapRepository { providerId: String, rateType: RateType, toAddress: String, + refundAddress: String, ): AggregatedSwapDataModel fun getNativeTokenForNetwork(networkId: String): CryptoCurrency From d10cf0ac2e75a4d1ef9641fc8a7d6b3f93978d47 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Nov 2023 18:28:09 +0300 Subject: [PATCH 049/139] Updated on 2026-08-14 --- .../layout/layout_single_wallet_balance.xml | 2 +- .../res/layout/view_onboarding_tv_balance.xml | 2 +- core/res/src/main/res/values-ru/strings.xml | 7 +- .../src/main/res/values-zh-rTW/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 27 ++- .../bottomsheets/TangemBottomSheet.kt | 6 +- .../ui/components/inputrow/InputRowApprox.kt | 150 ++++++++++++ .../components/inputrow/InputRowBestRate.kt | 221 ++++++++++++++++++ .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../res/drawable/ic_alert_triangle_20.xml | 10 + .../ui/src/main/res/drawable/ic_approx_24.xml | 9 + .../main/res/drawable/ic_exclamation_24.xml | 12 + .../src/main/res/drawable/ic_forward_24.xml | 9 + .../domain/models/domain/ExchangeStatus.kt | 20 ++ features/tokendetails/impl/build.gradle.kts | 2 + .../state/SwapTransactionsState.kt | 31 +++ .../ui/components/TokenDetailsBalanceBlock.kt | 2 +- .../components/exchange/ExchangeEstimate.kt | 67 ++++++ .../components/exchange/ExchangeProvider.kt | 41 ++++ .../exchange/ExchangeStatusBlock.kt | 209 +++++++++++++++++ .../exchange/ExchangeStatusBottomSheet.kt | 84 +++++++ .../exchange/ExchangeStatusItems.kt | 180 ++++++++++++++ .../viewmodels/TokenDetailsViewModel.kt | 33 ++- 23 files changed, 1117 insertions(+), 10 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt create mode 100644 core/ui/src/main/res/drawable/ic_alert_triangle_20.xml create mode 100644 core/ui/src/main/res/drawable/ic_approx_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_exclamation_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_forward_24.xml create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt diff --git a/app/src/main/res/layout/layout_single_wallet_balance.xml b/app/src/main/res/layout/layout_single_wallet_balance.xml index c3a75693bc..d3c6de4ff3 100644 --- a/app/src/main/res/layout/layout_single_wallet_balance.xml +++ b/app/src/main/res/layout/layout_single_wallet_balance.xml @@ -21,7 +21,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="8dp" - android:text="@string/onboarding_balance_title" + android:text="@string/common_balance_title" android:textColor="@color/text_tertiary" android:textSize="14sp" android:textStyle="bold" diff --git a/app/src/main/res/layout/view_onboarding_tv_balance.xml b/app/src/main/res/layout/view_onboarding_tv_balance.xml index f421e4d992..f84e954980 100644 --- a/app/src/main/res/layout/view_onboarding_tv_balance.xml +++ b/app/src/main/res/layout/view_onboarding_tv_balance.xml @@ -12,7 +12,7 @@ android:elevation="0dp" android:fontFamily="sans-serif-medium" android:letterSpacing="0.036" - android:text="@string/onboarding_balance_title" + android:text="@string/common_balance_title" android:textAllCaps="true" android:textColor="@color/text_secondary" android:textSize="14sp" diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 857c2f4ff1..325b697ba9 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -192,6 +192,11 @@ Недоступен для обмена с %s Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами Выберите провайдера + Чтобы узнать причину, посетите сайт провайдера + Операция не выполнена провайдером + Посетите сайт провайдера для проверки + Провайдер: требуется верификация + Провайдер Лучший курс Требуется разрешение Информация ниже не является обязательной. Вы можете стереть её, если хотите. @@ -266,7 +271,7 @@ Введенные коды доступа не совпадают Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить? Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас. - Баланс + Баланс Добавить резервную карту Сканировать карту #%d Создать резервную копию diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index b80f9ce410..0b9c4e90a9 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -178,7 +178,7 @@ 輸入的訪問密碼與初始訪問密碼不匹配 您已添加一張備用卡。備份過程完成後,您將無法添加更多備份卡。如果您還有一張卡,請將其添加到備份中。您想繼續備份過程嗎? 備份過程已部分完成。你現在不能退出 - 餘額 + 餘額 添加備用卡 掃描卡片 #%d 立即備份 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 6bd6a2445a..e88c410260 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -192,7 +192,30 @@ Unavailable for swap from %s Providers facilitate transactions, ensuring smooth and efficient token exchanges Choose Provider - Best Rate + Estimated amount + Visit provider’s website to see why + Operation failed by provider + Visit provider’s website for verification + Provider: Verification needed + Confirmed + Confirming + Exchanged + Exchanging + Failed + Deposit received + Awaiting deposit + Sending to you + Sent + Provider-sourced data. Estimated amount subject to change. + Exchange status + Verified + Verification required + Fetching best rates... + Floating rate + Go to provider + Provider + Best rate + Exchange by %s Permission Needed The following information is optional. You can erase it if you don\'t want to share it. Tell us what functions you are missing, and we will try to help you. @@ -267,7 +290,7 @@ Entered access code didn\'t match the initial access code You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process? The backup process is partly complete. You can\'t exit it now. - Balance + Balance Add a backup card Scan the card #%d Backup now diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 2d9d85b164..5278a24911 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color import com.tangem.core.ui.res.TangemTheme import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -20,6 +21,7 @@ import kotlinx.coroutines.launch @Composable inline fun TangemBottomSheet( config: TangemBottomSheetConfig, + color: Color = TangemTheme.colors.background.primary, crossinline content: @Composable ColumnScope.(T) -> Unit, ) { var isVisible by remember { mutableStateOf(value = config.isShow) } @@ -29,9 +31,9 @@ inline fun TangemBottomSheet( ModalBottomSheet( onDismissRequest = config.onDismissRequest, sheetState = sheetState, - containerColor = TangemTheme.colors.background.primary, + containerColor = color, shape = TangemTheme.shapes.bottomSheetLarge, - dragHandle = { TangemBottomSheetDraggableHeader() }, + dragHandle = { TangemBottomSheetDraggableHeader(color) }, ) { content(config.content) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt new file mode 100644 index 0000000000..3b085833a3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt @@ -0,0 +1,150 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * [Input Row Approx](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2207-810&mode=design&t=fM1ZU6zQF6g3CaTv-4) + * + * @param leftIcon left token state + * @param leftTitle left token title + * @param leftSubtitle left token subtitle + * @param rightIcon right token state + * @param rightTitle right token title + * @param rightSubtitle right token subtitle + * @param modifier composable modifier + * @param showDivider show divider + */ +@Suppress("LongParameterList") +@Composable +fun InputRowApprox( + leftIcon: TokenIconState, + leftTitle: TextReference, + leftSubtitle: TextReference, + rightIcon: TokenIconState, + rightTitle: TextReference, + rightSubtitle: TextReference, + modifier: Modifier = Modifier, + showDivider: Boolean = false, +) { + DividerContainer( + showDivider = showDivider, + modifier = modifier, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .padding(TangemTheme.dimens.spacing12) + .fillMaxWidth(), + ) { + InputRowApproxItem( + iconState = leftIcon, + title = leftTitle, + subtitle = leftSubtitle, + ) + Icon( + painter = painterResource(id = R.drawable.ic_approx_24), + contentDescription = null, + tint = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding( + horizontal = TangemTheme.dimens.spacing8, + vertical = TangemTheme.dimens.spacing10, + ), + ) + InputRowApproxItem( + iconState = rightIcon, + title = rightTitle, + subtitle = rightSubtitle, + ) + } + } +} + +@Composable +private fun InputRowApproxItem( + iconState: TokenIconState, + title: TextReference, + subtitle: TextReference, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + ) { + TokenIcon( + state = iconState, + modifier = Modifier + .size(TangemTheme.dimens.size36), + ) + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing12, + ), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing2), + ) + } + } +} + +//region Preview +@Preview +@Composable +private fun InputRowApproxPreview_Light() { + TangemTheme { + InputRowApprox( + leftIcon = TokenIconState.Loading, + leftTitle = TextReference.Str("Left title"), + leftSubtitle = TextReference.Str("Left subtitle"), + rightIcon = TokenIconState.Loading, + rightTitle = TextReference.Str("Right title"), + rightSubtitle = TextReference.Str("Right subtitle"), + modifier = Modifier + .background(TangemTheme.colors.background.action), + ) + } +} + +@Preview +@Composable +private fun InputRowApproxPreview_Dark() { + TangemTheme(isDark = true) { + InputRowApprox( + leftIcon = TokenIconState.Loading, + leftTitle = TextReference.Str("Left title"), + leftSubtitle = TextReference.Str("Left subtitle"), + rightIcon = TokenIconState.Loading, + rightTitle = TextReference.Str("Right title"), + rightSubtitle = TextReference.Str("Right subtitle"), + modifier = Modifier + .background(TangemTheme.colors.background.action), + ) + } +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt new file mode 100644 index 0000000000..620e3346cf --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt @@ -0,0 +1,221 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * [Input Row Best Rate](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-889&mode=dev) + * + * @param imageUrl image source url + * @param title title + * @param titleExtra title extra + * @param subtitle subtitle + * @param modifier composable modifier + * @param showTag show tag + * @param showDivider show divider + * @param onIconClick icon click + */ +@Composable +fun InputRowBestRate( + imageUrl: String, + title: TextReference, + titleExtra: TextReference, + subtitle: TextReference, + modifier: Modifier = Modifier, + showTag: Boolean = false, + showDivider: Boolean = false, + onIconClick: (() -> Unit)? = null, +) { + DividerContainer( + showDivider = showDivider, + modifier = modifier, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(TangemTheme.dimens.spacing12), + ) { + InnerIcon(imageUrl = imageUrl) + Column( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12), + ) { + InnerTitle( + title = title, + titleExtra = titleExtra, + showTag = showTag, + ) + Text( + text = subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) + } + SpacerWMax() + onIconClick?.let { + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing10) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onClick = onIconClick, + ), + ) + } + } + } +} + +@Composable +private fun InnerTitle(title: TextReference, titleExtra: TextReference, showTag: Boolean = false) { + Row { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = titleExtra.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing4), + ) + if (showTag) { + Text( + text = stringResource(R.string.express_provider_best_rate), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.icon.accent, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing4) + .background( + color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), + shape = RoundedCornerShape(TangemTheme.dimens.radius20), + ) + .padding(horizontal = TangemTheme.dimens.spacing6), + ) + } + } +} + +@Composable +private fun InnerIcon(imageUrl: String) { + SubcomposeAsyncImage( + modifier = Modifier.size(TangemTheme.dimens.size40), + model = ImageRequest.Builder(context = LocalContext.current) + .data(imageUrl) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { LoadingIcon() }, + error = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.tertiary, + shape = CircleShape, + ), + ) + }, + contentDescription = null, + ) +} + +//region preview +@Preview +@Composable +private fun InputRowBestRatePreview_Light( + @PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData, +) { + TangemTheme { + InputRowBestRate( + imageUrl = "", + title = data.title, + titleExtra = data.titleExtra, + subtitle = data.subtitle, + showTag = data.showTag, + onIconClick = data.iconClick, + modifier = Modifier + .background(TangemTheme.colors.background.action), + ) + } +} + +@Preview +@Composable +private fun InputRowBestRatePreview_Dark( + @PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData, +) { + TangemTheme(isDark = true) { + InputRowBestRate( + imageUrl = "", + title = data.title, + titleExtra = data.titleExtra, + subtitle = data.subtitle, + showTag = data.showTag, + onIconClick = data.iconClick, + modifier = Modifier + .background(TangemTheme.colors.background.action), + ) + } +} + +private data class InputRowBestRatePreviewData( + val title: TextReference, + val titleExtra: TextReference, + val showTag: Boolean, + val subtitle: TextReference, + val iconClick: (() -> Unit)?, +) + +private class InputRowBestRatePreviewDataProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InputRowBestRatePreviewData( + title = TextReference.Str("1inch"), + titleExtra = TextReference.Str("DEX"), + subtitle = TextReference.Str("0,64554846 DAI ≈ 1 MATIC "), + showTag = true, + iconClick = {}, + ), + InputRowBestRatePreviewData( + title = TextReference.Str("ChangeNow"), + titleExtra = TextReference.Str("CEX"), + subtitle = TextReference.Str("0,64554846 DAI ≈ 1 MATIC "), + showTag = false, + iconClick = null, + ), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 2fc319885d..e68dfd96a0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -41,6 +41,7 @@ data class TangemDimens internal constructor( val size0: Dp = 0.dp, val size0_5: Dp = 0.5.dp, val size1: Dp = 1.dp, + val size1_5: Dp = 1.5.dp, val size2: Dp = 2.dp, val size4: Dp = 4.dp, val size5: Dp = 5.dp, diff --git a/core/ui/src/main/res/drawable/ic_alert_triangle_20.xml b/core/ui/src/main/res/drawable/ic_alert_triangle_20.xml new file mode 100644 index 0000000000..dfe1b75e71 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_alert_triangle_20.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_approx_24.xml b/core/ui/src/main/res/drawable/ic_approx_24.xml new file mode 100644 index 0000000000..30eb78ec9d --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_approx_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_exclamation_24.xml b/core/ui/src/main/res/drawable/ic_exclamation_24.xml new file mode 100644 index 0000000000..d7d5bdca87 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_exclamation_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_forward_24.xml b/core/ui/src/main/res/drawable/ic_forward_24.xml new file mode 100644 index 0000000000..1a35b84bfb --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_forward_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt new file mode 100644 index 0000000000..a74e8fbb6a --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.swap.domain.models.domain + +data class ExchangeStatusModel( + val providerId: Int, + val status: ExchangeStatus? = null, + val txId: String? = null, + val txUrl: String? = null, +) + +enum class ExchangeStatus { + New, + Waiting, + Confirming, + Verifying, + Exchanging, + Failed, + Sending, + Finished, + Refunded, +} \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index bc5b2cda79..c19e3f3577 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.ui.utils) + implementation(deps.compose.constraintLayout) implementation(deps.arrow.core) implementation(deps.jodatime) @@ -68,4 +69,5 @@ dependencies { /** Feature Apis */ implementation(projects.features.tokendetails.api) implementation(projects.features.send.api) + implementation(projects.features.swap.domain) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt new file mode 100644 index 0000000000..8b4fb5c8a5 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +import kotlinx.collections.immutable.PersistentList +import java.math.BigDecimal + +internal data class SwapTransactionsState( + val txId: String, + val providerId: Int, + val txUrl: String? = null, + val rate: BigDecimal, + val timestamp: Long, + val status: PersistentList, + val activeStatus: ExchangeStatus?, + val toCryptoAmount: String, + val toFiatAmount: String, + val toCurrencyIcon: TokenIconState, + val fromCryptoAmount: String, + val fromFiatAmount: String, + val fromCurrencyIcon: TokenIconState, + val onClick: () -> Unit, + val onGoToProviderClick: () -> Unit, +) + +internal class ExchangeStatusState( + val status: ExchangeStatus, + val text: String, + val isActive: Boolean, + val isDone: Boolean, +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 8280f9aeab..f9911ae521 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -39,7 +39,7 @@ internal fun TokenDetailsBalanceBlock( start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ), - text = stringResource(id = R.string.onboarding_balance_title), + text = stringResource(id = R.string.common_balance_title), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, maxLines = 1, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt new file mode 100644 index 0000000000..c33b4fc290 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt @@ -0,0 +1,67 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange + +import androidx.compose.foundation.background +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.res.stringResource +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.inputrow.InputRowApprox +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.tokendetails.impl.R + +@Suppress("LongParameterList") +@Composable +internal fun ExchangeEstimate( + timestamp: TextReference, + fromTokenIconState: TokenIconState, + toTokenIconState: TokenIconState, + fromCryptoAmount: TextReference, + toCryptoAmount: TextReference, + fromFiatAmount: TextReference, + toFiatAmount: TextReference, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing14, + bottom = TangemTheme.dimens.spacing2, + ), + ) { + Text( + text = stringResource(id = R.string.express_estimated_amount), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + text = timestamp.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + InputRowApprox( + leftIcon = fromTokenIconState, + leftTitle = fromCryptoAmount, + leftSubtitle = fromFiatAmount, + rightIcon = toTokenIconState, + rightTitle = toCryptoAmount, + rightSubtitle = toFiatAmount, + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt new file mode 100644 index 0000000000..26c7393a85 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.R +import com.tangem.core.ui.components.inputrow.InputRowBestRate +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun ExchangeProvider(providerName: TextReference, providerType: TextReference, imageUrl: String) { + Column( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + Text( + text = stringResource(id = R.string.express_provider), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + ), + ) + InputRowBestRate( + imageUrl = imageUrl, + title = providerName, + titleExtra = providerType, + subtitle = TextReference.Res(R.string.express_floating_rate), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt new file mode 100644 index 0000000000..a583d1cb56 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -0,0 +1,209 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +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.CircleShape +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Icon +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.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState +import com.tangem.features.tokendetails.impl.R +import kotlinx.collections.immutable.PersistentList + +@Composable +internal fun ExchangeStatusBlock( + status: PersistentList, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding( + vertical = TangemTheme.dimens.spacing14, + horizontal = TangemTheme.dimens.spacing12, + ), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing16), + ) { + Text( + text = stringResource(id = R.string.common_balance_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + SpacerWMax() + Row( + modifier = Modifier.clickable { onClick() }, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_top_right_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(TangemTheme.dimens.spacing16) + .padding(end = TangemTheme.dimens.spacing2), + ) + Text( + text = stringResource(id = R.string.express_go_to_provider), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + + status.forEachIndexed { index, item -> + ExchangeStatusStep( + stepStatus = item, + isLast = index == status.lastIndex, + ) + } + } +} + +@Composable +private fun ExchangeStatusStep( + stepStatus: ExchangeStatusState, + modifier: Modifier = Modifier, + isLast: Boolean = false, +) { + Row(modifier = modifier) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AnimatedContent( + targetState = stepStatus, + label = "Exchange Step Change Success", + modifier = Modifier + .size(TangemTheme.dimens.size20), + ) { + when { + it.status == ExchangeStatus.Failed && !it.isDone -> ExchangeStepWaringOrError( + iconRes = R.drawable.ic_close_24, + color = TangemTheme.colors.icon.warning, + ) + it.status == ExchangeStatus.Verifying && !it.isDone -> ExchangeStepWaringOrError( + iconRes = R.drawable.ic_exclamation_24, + color = TangemTheme.colors.icon.attention, + ) + it.isDone -> ExchangeStepSuccess() + it.isActive -> ExchangeStepInProgress() + else -> ExchangeStepDefault() + } + } + if (!isLast) { + ExchangeStepSeparator() + } + } + ExchangeStatusStepText(stepStatus) + } +} + +@Composable +private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) { + val textColor = when { + stepStatus.status == ExchangeStatus.Failed && !stepStatus.isDone -> TangemTheme.colors.icon.warning + stepStatus.status == ExchangeStatus.Verifying && !stepStatus.isDone -> TangemTheme.colors.icon.attention + stepStatus.isDone -> TangemTheme.colors.text.primary1 + !stepStatus.isActive -> TangemTheme.colors.text.disabled + else -> TangemTheme.colors.text.primary1 + } + + Text( + text = stepStatus.text, + style = TangemTheme.typography.body2, + color = textColor, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12), + ) +} + +@Composable +private fun ExchangeStepDefault() { + Box( + modifier = Modifier + .border( + width = TangemTheme.dimens.size1_5, + color = TangemTheme.colors.field.focused, + shape = CircleShape, + ) + .padding(TangemTheme.dimens.spacing2), + ) +} + +@Composable +private fun ExchangeStepSuccess() { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier + .border( + width = TangemTheme.dimens.size1_5, + color = TangemTheme.colors.field.focused, + shape = CircleShape, + ) + .padding(TangemTheme.dimens.spacing2), + ) +} + +@Composable +private fun ExchangeStepWaringOrError(color: Color, @DrawableRes iconRes: Int) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = color, + modifier = Modifier + .border( + width = TangemTheme.dimens.size1_5, + color = color, + shape = CircleShape, + ) + .padding(TangemTheme.dimens.spacing2), + ) +} + +@Composable +private fun ExchangeStepInProgress() { + CircularProgressIndicator( + color = TangemTheme.colors.icon.primary1, + strokeWidth = TangemTheme.dimens.size2, + modifier = Modifier + .padding(TangemTheme.dimens.spacing2) + .size(TangemTheme.dimens.size14), + ) +} + +@Composable +private fun ExchangeStepSeparator() { + Box( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing2) + .size( + width = TangemTheme.dimens.size1_5, + height = TangemTheme.dimens.size10, + ) + .background( + color = TangemTheme.colors.field.focused, + shape = CircleShape, + ), + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt new file mode 100644 index 0000000000..5705f00a63 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -0,0 +1,84 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH10 +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState + +@Composable +internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + color = TangemTheme.colors.background.tertiary, + ) { content: ExchangeStatusBottomSheetConfig -> + ExchangeStatusBottomSheetContent(content = content) + } +} + +@Composable +private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) { + val config = content.value + Column( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + SpacerH10() + Text( + text = stringResource(id = R.string.express_exchange_status_title), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.align(CenterHorizontally), + ) + SpacerH10() + Text( + text = stringResource(id = R.string.express_exchange_status_subtitle), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .align(CenterHorizontally), + ) + SpacerH16() + val timestamp = config.timestamp + ExchangeEstimate( + timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"), + fromTokenIconState = config.fromCurrencyIcon, + toTokenIconState = config.toCurrencyIcon, + fromCryptoAmount = TextReference.Str(config.fromCryptoAmount), + toCryptoAmount = TextReference.Str(config.toCryptoAmount), + fromFiatAmount = TextReference.Str(config.fromFiatAmount), + toFiatAmount = TextReference.Str(config.toFiatAmount), + ) + SpacerH12() + // todo replace with real provider data + ExchangeProvider( + providerName = TextReference.Str(config.providerId.toString()), + providerType = TextReference.Str("CEX"), + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/changenow_512.png", + ) + SpacerH12() + ExchangeStatusBlock( + status = config.status, + onClick = config.onGoToProviderClick, + ) + SpacerH24() + } +} + +internal data class ExchangeStatusBottomSheetConfig( + val value: SwapTransactionsState, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt new file mode 100644 index 0000000000..95ac42480e --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -0,0 +1,180 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Visibility +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState +import com.tangem.features.tokendetails.impl.R +import kotlinx.collections.immutable.PersistentList + +@OptIn(ExperimentalFoundationApi::class) +internal fun LazyListScope.swapTransactionsItems( + swapTxs: PersistentList, + modifier: Modifier = Modifier, +) { + if (swapTxs.isNotEmpty()) { + items( + count = swapTxs.size, + key = { swapTxs[it].txId }, + contentType = { swapTxs[it]::class.java }, + ) { + val item = swapTxs[it] + + val (iconRes, tint) = when (item.activeStatus) { + ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention + ExchangeStatus.Failed -> R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning + else -> null to null + } + + ExchangeStatusItem( + providerName = item.providerId.toString(), + fromTokenIconState = item.fromCurrencyIcon, + toTokenIconState = item.toCurrencyIcon, + fromAmount = item.fromCryptoAmount, + toAmount = item.toCryptoAmount, + onClick = item.onClick, + infoIconRes = iconRes, + infoIconTint = tint, + modifier = modifier.animateItemPlacement(), + ) + } + } +} + +@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod", "LongParameterList") +@Composable +private fun ExchangeStatusItem( + providerName: String, + fromTokenIconState: TokenIconState, + toTokenIconState: TokenIconState, + fromAmount: String, + toAmount: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + @DrawableRes infoIconRes: Int? = null, + infoIconTint: Color? = null, +) { + ConstraintLayout( + modifier = modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable { onClick() } + .padding(TangemTheme.dimens.spacing12), + ) { + val (titleRef, iconRef, infoIconRef, swapIconRef, fromRef, toRef, fromIconRef, toIconRef) = createRefs() + val padding6 = TangemTheme.dimens.spacing6 + + Text( + text = stringResource(id = R.string.express_exchange_by, providerName), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.constrainAs(titleRef) { + start.linkTo(parent.start) + top.linkTo(parent.top) + }, + ) + TokenIcon( + state = fromTokenIconState, + shouldDisplayNetwork = false, + modifier = Modifier + .size(TangemTheme.dimens.size20) + .constrainAs(fromIconRef) { + start.linkTo(parent.start) + top.linkTo(titleRef.bottom, padding6) + bottom.linkTo(parent.bottom) + }, + ) + Text( + text = fromAmount, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.constrainAs(fromRef) { + start.linkTo(fromIconRef.end, padding6) + top.linkTo(titleRef.bottom, padding6) + bottom.linkTo(parent.bottom) + }, + ) + Icon( + painter = painterResource(id = R.drawable.ic_forward_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(TangemTheme.dimens.size12) + .constrainAs(swapIconRef) { + start.linkTo(fromRef.end, padding6) + top.linkTo(titleRef.bottom, padding6) + bottom.linkTo(parent.bottom) + }, + ) + TokenIcon( + state = toTokenIconState, + shouldDisplayNetwork = false, + modifier = Modifier + .size(TangemTheme.dimens.size20) + .constrainAs(toIconRef) { + start.linkTo(swapIconRef.end, padding6) + top.linkTo(titleRef.bottom, padding6) + bottom.linkTo(parent.bottom) + }, + ) + Text( + text = toAmount, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.constrainAs(toRef) { + start.linkTo(toIconRef.end, padding6) + top.linkTo(titleRef.bottom, padding6) + bottom.linkTo(parent.bottom) + }, + ) + Icon( + painter = painterResource(id = infoIconRes ?: R.drawable.ic_alert_triangle_20), + contentDescription = null, + tint = infoIconTint ?: TangemTheme.colors.icon.informative, + modifier = Modifier + .size(TangemTheme.dimens.size20) + .constrainAs(infoIconRef) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + end.linkTo(iconRef.start) + visibility = if (infoIconRes == null) { + Visibility.Gone + } else { + Visibility.Visible + } + }, + ) + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(TangemTheme.dimens.size24) + .constrainAs(iconRef) { + end.linkTo(parent.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + }, + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index b1496b2e0c..55cb6c48be 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -475,4 +475,35 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onCloseRentInfoNotification() { uiState = stateFactory.getStateWithRemovedRentNotification() } -} \ No newline at end of file +} + +// +// val text = when (stepStatus.status) { +// ExchangeStatus.Failed -> stringResource(id = R.string.express_exchange_status_failed) +// ExchangeStatus.Verifying -> if (stepStatus.isDone) { +// stringResource(id = R.string.express_exchange_status_verified) +// } else { +// stringResource(id = R.string.express_exchange_status_verifying) +// } +// ExchangeStatus.New, ExchangeStatus.Waiting -> if (stepStatus.isDone) { +// stringResource(id = R.string.express_exchange_status_received) +// } else { +// stringResource(id = R.string.express_exchange_status_receiving) +// } +// ExchangeStatus.Confirming -> if (stepStatus.isDone) { +// stringResource(id = R.string.express_exchange_status_confirmed) +// } else { +// stringResource(id = R.string.express_exchange_status_confirming) +// } +// ExchangeStatus.Exchanging -> if (stepStatus.isDone) { +// stringResource(id = R.string.express_exchange_status_exchanged) +// } else { +// stringResource(id = R.string.express_exchange_status_exchanging) +// } +// ExchangeStatus.Sending -> if (stepStatus.isDone) { +// stringResource(id = R.string.express_exchange_status_sent) +// } else { +// stringResource(id = R.string.express_exchange_status_sending) +// } +// else -> "" +// } \ No newline at end of file From 1fdc1d07b290282c60c05dd9d8272011512dcf3d Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 25 Nov 2023 02:09:28 +0200 Subject: [PATCH 050/139] Updated on 2026-08-14 --- .../api/express/TangemExpressApi.kt | 2 + .../tangem/feature/swap/SwapRepositoryImpl.kt | 4 ++ .../feature/swap/domain/SwapInteractor.kt | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 59 ++++++++++++++++--- .../feature/swap/domain/SwapRepository.kt | 2 + .../feature/swap/viewmodels/SwapViewModel.kt | 1 + .../tangem/lib/crypto/models/SwapTxData.kt | 1 + 7 files changed, 63 insertions(+), 7 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 8fff666e26..ffe39459b5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -32,6 +32,7 @@ interface TangemExpressApi { @Query("toContractAddress") toContractAddress: String, @Query("toNetwork") toNetwork: String, @Query("fromAmount") fromAmount: String, + @Query("fromDecimals") fromDecimals: Int, @Query("providerId") providerId: String, @Query("rateType") rateType: String, ): ApiResponse @@ -43,6 +44,7 @@ interface TangemExpressApi { @Query("toContractAddress") toContractAddress: String, @Query("toNetwork") toNetwork: String, @Query("fromAmount") fromAmount: String, + @Query("fromDecimals") fromDecimals: Int, @Query("providerId") providerId: String, @Query("rateType") rateType: String, @Query("toAddress") toAddress: String, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index b143fcf4d4..c77ab826b6 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -145,6 +145,7 @@ internal class SwapRepositoryImpl @Inject constructor( toContractAddress: String, toNetwork: String, fromAmount: String, + fromDecimals: Int, providerId: String, rateType: RateType, ): AggregatedSwapDataModel { @@ -156,6 +157,7 @@ internal class SwapRepositoryImpl @Inject constructor( toContractAddress = toContractAddress, toNetwork = toNetwork, fromAmount = fromAmount, + fromDecimals = fromDecimals, providerId = providerId, rateType = rateType.name.lowercase(), ).getOrThrow() @@ -182,6 +184,7 @@ internal class SwapRepositoryImpl @Inject constructor( toContractAddress: String, toNetwork: String, fromAmount: String, + fromDecimals: Int, providerId: String, rateType: RateType, toAddress: String, @@ -195,6 +198,7 @@ internal class SwapRepositoryImpl @Inject constructor( toContractAddress = toContractAddress, toNetwork = toNetwork, fromAmount = fromAmount, + fromDecimals = fromDecimals, providerId = providerId, rateType = rateType.name.lowercase(), toAddress = toAddress, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 9a6d547675..8664f2f1ba 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -69,6 +69,7 @@ interface SwapInteractor { @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun onSwap( + exchangeProviderType: ExchangeProviderType, networkId: String, swapStateData: SwapStateData, currencyToSend: CryptoCurrency, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index be2096f0c4..776052329a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -305,6 +305,7 @@ internal class SwapInteractorImpl @Inject constructor( ) } else { provider to loadQuoteData( + exchangeProviderType = ExchangeProviderType.DEX, networkId = networkId, amount = amount, fromTokenStatus = fromToken, @@ -326,6 +327,7 @@ internal class SwapInteractorImpl @Inject constructor( isBalanceWithoutFeeEnough: Boolean, ): Pair { return provider to loadQuoteData( + exchangeProviderType = ExchangeProviderType.CEX, networkId = networkId, amount = amount, fromTokenStatus = fromToken, @@ -338,6 +340,32 @@ internal class SwapInteractorImpl @Inject constructor( @Deprecated("used in old swap mechanism") override suspend fun onSwap( + exchangeProviderType: ExchangeProviderType, + networkId: String, + swapStateData: SwapStateData, + currencyToSend: CryptoCurrency, + currencyToGet: CryptoCurrency, + amountToSwap: String, + fee: TxFee, + ): TxState { + return when (exchangeProviderType) { + ExchangeProviderType.CEX -> { + onSwapCex() + } + ExchangeProviderType.DEX -> { + onSwapDex( + networkId = networkId, + swapStateData = swapStateData, + currencyToSend = currencyToSend, + currencyToGet = currencyToGet, + amountToSwap = amountToSwap, + fee = fee, + ) + } + } + } + + private suspend fun onSwapDex( networkId: String, swapStateData: SwapStateData, currencyToSend: CryptoCurrency, @@ -391,6 +419,10 @@ internal class SwapInteractorImpl @Inject constructor( } } + private fun onSwapCex(): TxState { + TODO() + } + @Deprecated("used in old swap mechanism") override fun getTokenBalance(networkId: String, token: CryptoCurrency): SwapAmount { return cache.getBalanceForToken( @@ -486,6 +518,7 @@ internal class SwapInteractorImpl @Inject constructor( */ @Suppress("LongParameterList") private suspend fun loadQuoteData( + exchangeProviderType: ExchangeProviderType, networkId: String, amount: SwapAmount, fromTokenStatus: CryptoCurrencyStatus, @@ -503,12 +536,13 @@ internal class SwapInteractorImpl @Inject constructor( toContractAddress = toToken.getContractAddress(), toNetwork = toToken.network.backendId, fromAmount = amount.toStringWithRightOffset(), + fromDecimals = amount.decimals, providerId = provider.providerId, rateType = RateType.FLOAT, ) - getState( + exchangeProviderType = exchangeProviderType, quoteDataModel = quotes, amount = amount, fromToken = fromTokenStatus, @@ -521,6 +555,7 @@ internal class SwapInteractorImpl @Inject constructor( } private suspend fun getState( + exchangeProviderType: ExchangeProviderType, quoteDataModel: AggregatedSwapDataModel, amount: SwapAmount, fromToken: CryptoCurrencyStatus, @@ -539,12 +574,21 @@ internal class SwapInteractorImpl @Inject constructor( toTokenAmount = quoteModel.toTokenAmount, swapStateData = null, ) - val quotesState = updatePermissionState( - networkId = networkId, - fromToken = fromToken.currency, - swapAmount = amount, - quotesLoadedState = swapState, - ) + + val quotesState = when (exchangeProviderType) { + ExchangeProviderType.DEX -> { + updatePermissionState( + networkId = networkId, + fromToken = fromToken.currency, + swapAmount = amount, + quotesLoadedState = swapState + ) + } + ExchangeProviderType.CEX -> { + swapState.copy(permissionState = PermissionDataState.Empty) + } + } + return quotesState.copy( preparedSwapConfigState = quotesState.preparedSwapConfigState.copy( isAllowedToSpend = isAllowedToSpend, @@ -587,6 +631,7 @@ internal class SwapInteractorImpl @Inject constructor( toContractAddress = toToken.currency.getContractAddress(), toNetwork = toToken.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), + fromDecimals = amount.decimals, providerId = provider.providerId, rateType = RateType.FLOAT, toAddress = toToken.value.networkAddress?.defaultAddress ?: "", diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 86d54285e5..933833637b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -21,6 +21,7 @@ interface SwapRepository { toContractAddress: String, toNetwork: String, fromAmount: String, + fromDecimals: Int, providerId: String, rateType: RateType, ): AggregatedSwapDataModel @@ -63,6 +64,7 @@ interface SwapRepository { toContractAddress: String, toNetwork: String, fromAmount: String, + fromDecimals: Int, providerId: String, rateType: RateType, toAddress: String, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 1d119785f8..c70fd5c2a2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -324,6 +324,7 @@ internal class SwapViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { swapInteractor.onSwap( + exchangeProviderType = requireNotNull(dataState.selectedProvider?.type), networkId = dataState.networkId, swapStateData = requireNotNull(dataState.swapDataModel), currencyToSend = requireNotNull(dataState.fromCryptoCurrency?.currency), diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/SwapTxData.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/SwapTxData.kt index 723169c990..4732f49fcb 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/SwapTxData.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/SwapTxData.kt @@ -13,6 +13,7 @@ import java.math.BigDecimal * @property amountToSend amount of tx * @property currencyToSend currency for tx */ +// TODO split to cex,dex data class SwapTxData( val networkId: String, val feeAmount: BigDecimal, From 7a1c799ab78348e77acffdcf351031ab2dcd4073 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 25 Nov 2023 02:45:38 +0200 Subject: [PATCH 051/139] Updated on 2026-08-14 --- .../java/com/tangem/datasource/api/express/TangemExpressApi.kt | 1 - .../src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt | 2 -- .../java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 1 - .../main/java/com/tangem/feature/swap/domain/SwapRepository.kt | 1 - 4 files changed, 5 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index ffe39459b5..2ecdbabbff 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -48,7 +48,6 @@ interface TangemExpressApi { @Query("providerId") providerId: String, @Query("rateType") rateType: String, @Query("toAddress") toAddress: String, - @Query("refundAddress") refundAddress: String, ): ApiResponse @GET("exchange-result") diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index c77ab826b6..a4bcb1fa99 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -188,7 +188,6 @@ internal class SwapRepositoryImpl @Inject constructor( providerId: String, rateType: RateType, toAddress: String, - refundAddress: String ): AggregatedSwapDataModel { return withContext(coroutineDispatcher.io) { try { @@ -202,7 +201,6 @@ internal class SwapRepositoryImpl @Inject constructor( providerId = providerId, rateType = rateType.name.lowercase(), toAddress = toAddress, - refundAddress = refundAddress ).getOrThrow() AggregatedSwapDataModel( dataModel = expressDataConverter.convert(response) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 776052329a..0f5b79b69e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -635,7 +635,6 @@ internal class SwapInteractorImpl @Inject constructor( providerId = provider.providerId, rateType = RateType.FLOAT, toAddress = toToken.value.networkAddress?.defaultAddress ?: "", - refundAddress = fromToken.value.networkAddress?.defaultAddress ?: "", ).let { val swapData = it.dataModel if (swapData != null) { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 933833637b..dd2d72e7df 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -68,7 +68,6 @@ interface SwapRepository { providerId: String, rateType: RateType, toAddress: String, - refundAddress: String, ): AggregatedSwapDataModel fun getNativeTokenForNetwork(networkId: String): CryptoCurrency From 9505a1404b88cf3b4f8d523575c9991d160b178c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 00:35:31 +0300 Subject: [PATCH 052/139] Updated on 2026-08-14 --- .../tap/proxy/TransactionManagerImpl.kt | 20 +- .../api/express/TangemExpressApi.kt | 1 - .../core/ui/components/rows/ActionRow.kt | 20 +- .../utils/CryptoCurrencyFormatExtensions.kt | 5 +- .../tangem/feature/swap/SwapRepositoryImpl.kt | 5 +- .../swap/converters/ExpressDataConverter.kt | 3 +- .../tangem/feature/swap/di/SwapDataModule.kt | 3 - features/swap/domain/build.gradle.kts | 3 + .../feature/swap/domain/SwapInteractor.kt | 20 +- .../feature/swap/domain/SwapInteractorImpl.kt | 318 ++++++++++++---- .../swap/domain/di/SwapDomainModule.kt | 5 + .../models/domain/ExpressTransactionModel.kt | 5 +- .../swap/domain/models/ui/AmountFormatter.kt | 2 +- .../swap/domain/models/ui/SwapState.kt | 28 +- .../feature/swap/models/SwapStateHolder.kt | 4 +- .../tangem/feature/swap/models/UiActions.kt | 5 +- .../states/ChooseFeeBottomSheetConfig.kt | 4 +- .../swap/models/states/FeeItemState.kt | 24 +- .../feature/swap/ui/ChooseFeeBottomSheet.kt | 21 +- .../com/tangem/feature/swap/ui/FeeItem.kt | 26 +- .../tangem/feature/swap/ui/StateBuilder.kt | 358 ++++++------------ .../com/tangem/feature/swap/ui/SwapScreen.kt | 4 + .../feature/swap/ui/SwapScreenContent.kt | 108 +----- .../swap/viewmodels/SwapProcessDataState.kt | 22 +- .../feature/swap/viewmodels/SwapViewModel.kt | 87 +++-- .../com/tangem/lib/crypto/models/ProxyFees.kt | 17 +- 26 files changed, 576 insertions(+), 542 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index b05033931a..33823b5d36 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -224,20 +224,18 @@ class TransactionManagerImpl( // for not EVM blockchains set gasLimit ZERO for now when (fee.data) { is TransactionFee.Single -> { - val fee = (fee.data as TransactionFee.Single).normal + val normalFee = (fee.data as TransactionFee.Single).normal val singleFee = ProxyFee( gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = fee.amount), + fee = convertToProxyAmount(amount = normalFee.amount), ) - ProxyFees( - minFee = singleFee, - normalFee = singleFee, - priorityFee = singleFee, + ProxyFees.SingleFee( + singleFee = singleFee, ) } is TransactionFee.Choosable -> { val choosableFee = fee.data as TransactionFee.Choosable - ProxyFees( + ProxyFees.MultipleFees( minFee = ProxyFee( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.minimum.amount), @@ -280,7 +278,7 @@ class TransactionManagerImpl( ).increaseBigIntegerByPercents(increaseBy) return when (val gasPrice = walletManager.getGasPrice()) { is Result.Success -> { - createProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain) + createMultipleProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain) } is Result.Failure -> { error(gasPrice.error.message ?: gasPrice.error.customMessage) @@ -316,7 +314,7 @@ class TransactionManagerImpl( fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) - ProxyFees( + ProxyFees.MultipleFees( minFee = minProxyFee, normalFee = normalProxyFee, priorityFee = priorityProxyFee, @@ -456,7 +454,7 @@ class TransactionManagerImpl( * @param gasLimit * @param blockchain */ - private fun createProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees { + private fun createMultipleProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees { val gasPriceNormal = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE) val gasPricePriority = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE) val feeMin = gasLimit.multiply(gasPrice).toBigDecimal( @@ -495,7 +493,7 @@ class TransactionManagerImpl( decimals = blockchain.decimals(), ), ) - return ProxyFees( + return ProxyFees.MultipleFees( minFee = minFee, normalFee = normalFee, priorityFee = priorityFee, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 2ecdbabbff..b56262221a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -8,7 +8,6 @@ import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Query -import java.math.BigDecimal /** * Interface of Tangem Express API (new swap mechanism) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt index 5bdaa8f68b..86817c690a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt @@ -19,7 +19,7 @@ import com.tangem.core.ui.res.TangemTheme * https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=Ygv5sohTTHYAQcBS-4 */ @Composable -fun SimpleActionRow(title: String, description: String, modifier: Modifier = Modifier) { +fun SimpleActionRow(title: String, description: String, modifier: Modifier = Modifier, isClickable: Boolean = true) { Box( modifier = modifier .background(color = TangemTheme.colors.background.action) @@ -44,14 +44,16 @@ fun SimpleActionRow(title: String, description: String, modifier: Modifier = Mod ) } - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_24), - contentDescription = null, - modifier = Modifier - .align(alignment = Alignment.CenterEnd) - .padding(end = TangemTheme.dimens.spacing12), - tint = TangemTheme.colors.icon.informative, - ) + if (isClickable) { + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + modifier = Modifier + .align(alignment = Alignment.CenterEnd) + .padding(end = TangemTheme.dimens.spacing12), + tint = TangemTheme.colors.icon.informative, + ) + } } } diff --git a/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt b/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt index 1e266be497..a1995e1712 100644 --- a/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt +++ b/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt @@ -25,7 +25,7 @@ fun BigDecimal.toFormattedString( @Suppress("MagicNumber") fun BigDecimal.toFormattedCurrencyString( decimals: Int, - currency: String, + currency: String? = null, roundingMode: RoundingMode = RoundingMode.DOWN, limitNumberOfDecimals: Boolean = true, ): String { @@ -38,7 +38,8 @@ fun BigDecimal.toFormattedCurrencyString( decimals = decimalsForRounding, roundingMode = roundingMode, ) - return "$formattedAmount $currency" + val formattedCurrency = currency?.let { " $it " } ?: "" + return "$formattedAmount$formattedCurrency" } fun BigDecimal.toFiatString( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index a4bcb1fa99..b5d8b00f73 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -13,8 +13,6 @@ import com.tangem.datasource.api.express.models.response.SwapPair import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders import com.tangem.datasource.api.oneinch.OneInchApi import com.tangem.datasource.api.oneinch.OneInchApiFactory -import com.tangem.datasource.api.oneinch.OneInchErrorsHandler -import com.tangem.datasource.api.oneinch.errors.OneIncResponseException import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.config.ConfigManager import com.tangem.domain.common.extensions.fromNetworkId @@ -41,7 +39,6 @@ internal class SwapRepositoryImpl @Inject constructor( private val tangemTechApi: TangemTechApi, private val tangemExpressApi: TangemExpressApi, private val oneInchApiFactory: OneInchApiFactory, - private val oneInchErrorsHandler: OneInchErrorsHandler, private val coroutineDispatcher: CoroutineDispatcherProvider, private val configManager: ConfigManager, private val walletManagersFacade: WalletManagersFacade, @@ -203,7 +200,7 @@ internal class SwapRepositoryImpl @Inject constructor( toAddress = toAddress, ).getOrThrow() AggregatedSwapDataModel( - dataModel = expressDataConverter.convert(response) + dataModel = expressDataConverter.convert(response), ) } catch (ex: Exception) { AggregatedSwapDataModel(null, mapErrors(ex.message)) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt index 0c4278683f..6746243002 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt @@ -24,7 +24,7 @@ class ExpressDataConverter : Converter { txId = transactionDto.txId, txTo = transactionDto.txTo, txFrom = requireNotNull(transactionDto.txFrom), - txData = requireNotNull(transactionDto.txData) + txData = requireNotNull(transactionDto.txData), ) } else { ExpressTransactionModel.CEX( @@ -37,5 +37,4 @@ class ExpressDataConverter : Converter { ) } } - } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index a70de3e545..261dcbb069 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -2,7 +2,6 @@ package com.tangem.feature.swap.di import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.oneinch.OneInchApiFactory -import com.tangem.datasource.api.oneinch.OneInchErrorsHandler import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.config.ConfigManager import com.tangem.domain.walletmanager.WalletManagersFacade @@ -26,7 +25,6 @@ class SwapDataModule { tangemTechApi: TangemTechApi, tangemExpressApi: TangemExpressApi, oneInchApiFactory: OneInchApiFactory, - oneInchErrorsHandler: OneInchErrorsHandler, coroutineDispatcher: CoroutineDispatcherProvider, configManager: ConfigManager, walletManagerFacade: WalletManagersFacade, @@ -36,7 +34,6 @@ class SwapDataModule { tangemTechApi = tangemTechApi, tangemExpressApi = tangemExpressApi, oneInchApiFactory = oneInchApiFactory, - oneInchErrorsHandler = oneInchErrorsHandler, coroutineDispatcher = coroutineDispatcher, configManager = configManager, walletManagersFacade = walletManagerFacade, diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index b757775fd0..00bcf936c1 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -23,6 +23,8 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.transaction) + implementation(projects.domain.legacy) /** Core modules */ implementation(projects.core.utils) @@ -35,4 +37,5 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) implementation(deps.timber) + implementation(deps.tangem.blockchain) } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 8664f2f1ba..1057373c25 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -7,7 +7,6 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* -import java.math.BigDecimal interface SwapInteractor { @@ -71,13 +70,21 @@ interface SwapInteractor { suspend fun onSwap( exchangeProviderType: ExchangeProviderType, networkId: String, - swapStateData: SwapStateData, + swapData: SwapDataModel, currencyToSend: CryptoCurrency, currencyToGet: CryptoCurrency, amountToSwap: String, fee: TxFee, ): TxState + suspend fun updateQuotesStateWithSelectedFee( + state: SwapState.QuotesLoadedState, + selectedFee: FeeType, + fromToken: CryptoCurrencyStatus, + amountToSwap: String, + networkId: String, + ): SwapState.QuotesLoadedState + /** * Returns token in wallet balance * @@ -88,14 +95,5 @@ interface SwapInteractor { fun isAvailableToSwap(networkId: String): Boolean - fun getSwapAmountForToken(amount: String, token: CryptoCurrency): SwapAmount - - suspend fun checkFeeIsEnough( - fee: BigDecimal?, - spendAmount: SwapAmount, - networkId: String, - fromToken: CryptoCurrency, - ): Boolean - fun getSelectedWallet(): UserWallet? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 0f5b79b69e..46ace65b78 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,6 +1,8 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency @@ -10,6 +12,8 @@ import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.cache.SwapDataCache @@ -24,10 +28,10 @@ import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.toFiatString -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -45,7 +49,9 @@ internal class SwapInteractorImpl @Inject constructor( private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val walletManagersFacade: WalletManagersFacade, private val quotesRepository: QuotesRepository, + private val dispatcher: CoroutineDispatcherProvider, ) : SwapInteractor { // TODO: Move to DI @@ -53,6 +59,10 @@ internal class SwapInteractorImpl @Inject constructor( AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository) } + private val getFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { + GetFeeUseCase(walletManagersFacade, dispatcher) + } + private val swapCurrencyConverter = SwapCurrencyConverter() private val amountFormatter = AmountFormatter() private var derivationPath: String? = null @@ -236,18 +246,11 @@ internal class SwapInteractorImpl @Inject constructor( createEmptyAmountState( networkId, fromToken.currency, - toToken.currency + toToken.currency, ) } } val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) - val fromTokenAddress = getTokenAddress(fromToken.currency) - val toTokenAddress = getTokenAddress(toToken.currency) - val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount) - if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { - allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) - transactionManager.updateWalletManager(networkId, derivationPath) - } val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null) when (provider.type) { @@ -256,12 +259,9 @@ internal class SwapInteractorImpl @Inject constructor( networkId = networkId, fromToken = fromToken, toToken = toToken, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, provider = provider, selectedFee = selectedFee, amount = amount, - isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, ) } @@ -272,8 +272,8 @@ internal class SwapInteractorImpl @Inject constructor( toToken = toToken, provider = provider, amount = amount, - isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + selectedFee = selectedFee, ) } } @@ -284,20 +284,21 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, - fromTokenAddress: String, - toTokenAddress: String, provider: SwapProvider, selectedFee: FeeType, amount: SwapAmount, - isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, ): Pair { + val fromTokenAddress = getTokenAddress(fromToken.currency) + val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount) + if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { + allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) + transactionManager.updateWalletManager(networkId, derivationPath) + } return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { provider to loadSwapData( provider = provider, networkId = networkId, - fromTokenAddress = fromTokenAddress, - toTokenAddress = toTokenAddress, fromToken = fromToken, toToken = toToken, amount = amount, @@ -313,6 +314,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, provider = provider, + selectedFee = selectedFee, ) } } @@ -323,8 +325,8 @@ internal class SwapInteractorImpl @Inject constructor( toToken: CryptoCurrencyStatus, provider: SwapProvider, amount: SwapAmount, - isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, + selectedFee: FeeType, ): Pair { return provider to loadQuoteData( exchangeProviderType = ExchangeProviderType.CEX, @@ -332,9 +334,10 @@ internal class SwapInteractorImpl @Inject constructor( amount = amount, fromTokenStatus = fromToken, toTokenStatus = toToken, - isAllowedToSpend = isAllowedToSpend, + isAllowedToSpend = true, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, provider = provider, + selectedFee = selectedFee, ) } @@ -342,7 +345,7 @@ internal class SwapInteractorImpl @Inject constructor( override suspend fun onSwap( exchangeProviderType: ExchangeProviderType, networkId: String, - swapStateData: SwapStateData, + swapData: SwapDataModel, currencyToSend: CryptoCurrency, currencyToGet: CryptoCurrency, amountToSwap: String, @@ -355,7 +358,7 @@ internal class SwapInteractorImpl @Inject constructor( ExchangeProviderType.DEX -> { onSwapDex( networkId = networkId, - swapStateData = swapStateData, + swapData = swapData, currencyToSend = currencyToSend, currencyToGet = currencyToGet, amountToSwap = amountToSwap, @@ -365,9 +368,39 @@ internal class SwapInteractorImpl @Inject constructor( } } + override suspend fun updateQuotesStateWithSelectedFee( + state: SwapState.QuotesLoadedState, + selectedFee: FeeType, + fromToken: CryptoCurrencyStatus, + amountToSwap: String, + networkId: String, + ): SwapState.QuotesLoadedState { + val amountDecimal = toBigDecimalOrNull(amountToSwap) + if (amountDecimal == null || amountDecimal.signum() == 0) { + return state + } + val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) + val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = state.txFee) + val isBalanceIncludeFeeEnough = + isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority) + val isFeeEnough = checkFeeIsEnough( + fee = feeByPriority, + spendAmount = amount, + networkId = networkId, + fromToken = fromToken.currency, + ) + return state.copy( + permissionState = PermissionDataState.Empty, + preparedSwapConfigState = state.preparedSwapConfigState.copy( + isBalanceEnough = isBalanceIncludeFeeEnough, + isFeeEnough = isFeeEnough, + ), + ) + } + private suspend fun onSwapDex( networkId: String, - swapStateData: SwapStateData, + swapData: SwapDataModel, currencyToSend: CryptoCurrency, currencyToGet: CryptoCurrency, amountToSwap: String, @@ -382,8 +415,8 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend = swapCurrencyConverter.convert(currencyToSend), feeAmount = fee.feeValue, gasLimit = fee.gasLimit, - destinationAddress = swapStateData.swapModel.transaction.txTo, - dataToSign = (swapStateData.swapModel.transaction as ExpressTransactionModel.DEX).txData, + destinationAddress = swapData.transaction.txTo, + dataToSign = (swapData.transaction as ExpressTransactionModel.DEX).txData, ), isSwap = true, derivationPath = derivationPath, @@ -405,7 +438,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend.symbol, ), toAmount = amountFormatter.formatSwapAmountToUI( - swapStateData.swapModel.toTokenAmount, + swapData.toTokenAmount, currencyToGet.symbol, ), txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath) ?: "", @@ -437,12 +470,6 @@ internal class SwapInteractorImpl @Inject constructor( return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId) } - @Deprecated("used in old swap mechanism") - override fun getSwapAmountForToken(amount: String, token: CryptoCurrency): SwapAmount { - val amountDecimal = requireNotNull(toBigDecimalOrNull(amount)) { "wrong amount format" } - return SwapAmount(amountDecimal, getTokenDecimals(token)) - } - @Deprecated("used in old swap mechanism") private suspend fun onSuccessLegacyFlow(currency: CryptoCurrency) { userWalletManager.addToken(swapCurrencyConverter.convert(currency), derivationPath) @@ -526,6 +553,7 @@ internal class SwapInteractorImpl @Inject constructor( provider: SwapProvider, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, + selectedFee: FeeType, ): SwapState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency @@ -541,7 +569,7 @@ internal class SwapInteractorImpl @Inject constructor( rateType = RateType.FLOAT, ) - getState( + getQuotesState( exchangeProviderType = exchangeProviderType, quoteDataModel = quotes, amount = amount, @@ -550,11 +578,13 @@ internal class SwapInteractorImpl @Inject constructor( networkId = networkId, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, + providerType = provider.type, + selectedFee = selectedFee, ) } } - private suspend fun getState( + private suspend fun getQuotesState( exchangeProviderType: ExchangeProviderType, quoteDataModel: AggregatedSwapDataModel, amount: SwapAmount, @@ -563,38 +593,59 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, + providerType: ExchangeProviderType, + selectedFee: FeeType, ): SwapState { val quoteModel = quoteDataModel.dataModel if (quoteModel != null) { + val txFee = if (providerType == ExchangeProviderType.CEX) { + getFeeForCex(amount, fromToken, networkId) + } else { + TxFeeState.Empty + } val swapState = updateBalances( networkId = networkId, fromTokenStatus = fromToken, toTokenStatus = toToken, fromTokenAmount = amount, toTokenAmount = quoteModel.toTokenAmount, - swapStateData = null, + swapData = null, + txFeeState = txFee, ) - val quotesState = when (exchangeProviderType) { + return when (exchangeProviderType) { ExchangeProviderType.DEX -> { - updatePermissionState( + val state = updatePermissionState( networkId = networkId, fromToken = fromToken.currency, swapAmount = amount, - quotesLoadedState = swapState + quotesLoadedState = swapState, + ) + state.copy( + preparedSwapConfigState = state.preparedSwapConfigState.copy( + isAllowedToSpend = isAllowedToSpend, + isBalanceEnough = isBalanceWithoutFeeEnough, + ), ) } ExchangeProviderType.CEX -> { - swapState.copy(permissionState = PermissionDataState.Empty) + val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFee) + val isFeeEnough = checkFeeIsEnough( + fee = feeByPriority, + spendAmount = amount, + networkId = networkId, + fromToken = fromToken.currency, + ) + swapState.copy( + permissionState = PermissionDataState.Empty, + preparedSwapConfigState = PreparedSwapConfigState( + isFeeEnough = isFeeEnough, + isAllowedToSpend = isAllowedToSpend, + isBalanceEnough = isBalanceWithoutFeeEnough, + ), + ) } } - - return quotesState.copy( - preparedSwapConfigState = quotesState.preparedSwapConfigState.copy( - isAllowedToSpend = isAllowedToSpend, - isBalanceEnough = isBalanceWithoutFeeEnough, - ), - ) } else { return SwapState.SwapError(quoteDataModel.error) } @@ -606,7 +657,7 @@ internal class SwapInteractorImpl @Inject constructor( val rates = getQuotes(nativeToken.id) return rates[nativeToken.id]?.fiatRate?.let { rate -> fees.map { fee -> - " (${fee.toFiatString(rate, appCurrency.symbol, true)})" + fee.toFiatString(rate, appCurrency.symbol, true) } }.orEmpty() } @@ -618,8 +669,6 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun loadSwapData( provider: SwapProvider, networkId: String, - fromTokenAddress: String, - toTokenAddress: String, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, amount: SwapAmount, @@ -647,11 +696,11 @@ internal class SwapInteractorImpl @Inject constructor( data = (swapData.transaction as ExpressTransactionModel.DEX).txData, derivationPath = derivationPath, ) - val txFeeState = proxyFeesToFeeState(networkId, feeData) - val feeByPriority = when (selectedFee) { - FeeType.NORMAL -> txFeeState.normalFee.feeValue - FeeType.PRIORITY -> txFeeState.priorityFee.feeValue + val txFeeState = when (feeData) { + is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId) + is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId) } + val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState) val isBalanceIncludeFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority) val isFeeEnough = checkFeeIsEnough( @@ -666,10 +715,8 @@ internal class SwapInteractorImpl @Inject constructor( toTokenStatus = toToken, fromTokenAmount = amount, toTokenAmount = swapData.toTokenAmount, - swapStateData = SwapStateData( - fee = txFeeState, - swapModel = swapData, - ), + swapData = swapData, + txFeeState = txFeeState, ) return swapState.copy( permissionState = PermissionDataState.Empty, @@ -692,7 +739,8 @@ internal class SwapInteractorImpl @Inject constructor( toTokenStatus: CryptoCurrencyStatus, fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, - swapStateData: SwapStateData?, + swapData: SwapDataModel?, + txFeeState: TxFeeState, ): SwapState.QuotesLoadedState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency @@ -719,11 +767,31 @@ internal class SwapInteractorImpl @Inject constructor( toRate = rates[toToken.id]?.fiatRate?.toDouble() ?: 0.0, ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), - swapDataModel = swapStateData, + swapDataModel = swapData, tangemFee = getTangemFee(), + txFee = txFeeState, ) } + private suspend fun getFeeForCex( + amount: SwapAmount, + fromToken: CryptoCurrencyStatus, + networkId: String, + ): TxFeeState { + getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> + val txFeeResult = getFeeUseCase( + amount = amount.value, + destination = fromToken.value.networkAddress?.defaultAddress ?: "", + userWalletId = userWalletId, + cryptoCurrency = fromToken.currency, + ).firstOrNull() + txFeeResult?.getOrNull()?.let { txFee -> + return txFee.toTxFeeState(networkId) + } + } + return TxFeeState.Empty + } + @Suppress("LongParameterList") private suspend fun updatePermissionState( networkId: String, @@ -759,9 +827,17 @@ internal class SwapInteractorImpl @Inject constructor( data = transactionData, derivationPath = derivationPath, ) - val feeState = proxyFeesToFeeState(networkId, feeData) + val feeState = when (feeData) { + is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId) + is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId) + } + val fee = when (feeState) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue + is TxFeeState.SingleFeeState -> feeState.fee.feeValue + } val isFeeEnough = checkFeeIsEnough( - fee = feeData.normalFee.fee.value, + fee = fee, spendAmount = SwapAmount.zeroSwapAmount(), networkId = networkId, fromToken = fromToken, @@ -801,30 +877,30 @@ internal class SwapInteractorImpl @Inject constructor( } } - private suspend fun proxyFeesToFeeState(networkId: String, proxyFees: ProxyFees): TxFeeState { - val normalFeeValue = proxyFees.minFee.fee.value // in swap for normal use min fee - val normalFeeGas = proxyFees.minFee.gasLimit.toInt() - val priorityFeeValue = proxyFees.normalFee.fee.value // in swap for priority use normal fee - val priorityFeeGas = proxyFees.normalFee.gasLimit.toInt() + private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState(networkId: String): TxFeeState { + val normalFeeValue = this.minFee.fee.value // in swap for normal use min fee + val normalFeeGas = this.minFee.gasLimit.toInt() + val priorityFeeValue = this.normalFee.fee.value // in swap for priority use normal fee + val priorityFeeGas = this.normalFee.gasLimit.toInt() val feesFiat = getFormattedFiatFees(networkId, normalFeeValue, priorityFeeValue) val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" } val priorityFiatFee = requireNotNull(feesFiat.getOrNull(1)) { "feesFiat item 1 couldn't be null" } + val networkCurrency = userWalletManager.getNetworkCurrency(networkId) val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = normalFeeValue, decimals = transactionManager.getNativeTokenDecimals(networkId), - currency = userWalletManager.getNetworkCurrency(networkId), ) val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = priorityFeeValue, decimals = transactionManager.getNativeTokenDecimals(networkId), - currency = userWalletManager.getNetworkCurrency(networkId), ) - return TxFeeState( + return TxFeeState.MultipleFeeState( normalFee = TxFee( feeValue = normalFeeValue, gasLimit = normalFeeGas, feeFiatFormatted = normalFiatFee, feeCryptoFormatted = normalCryptoFee, + cryptoSymbol = networkCurrency, feeType = FeeType.NORMAL, ), priorityFee = TxFee( @@ -832,11 +908,108 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = priorityFeeGas, feeFiatFormatted = priorityFiatFee, feeCryptoFormatted = priorityCryptoFee, + cryptoSymbol = networkCurrency, feeType = FeeType.PRIORITY, ), ) } + private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState(networkId: String): TxFeeState { + val normalFeeValue = this.singleFee.fee.value + val normalFeeGas = this.singleFee.gasLimit.toInt() + val networkCurrency = userWalletManager.getNetworkCurrency(networkId) + val feesFiat = getFormattedFiatFees(networkId, normalFeeValue) + val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" } + val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( + amount = normalFeeValue, + decimals = transactionManager.getNativeTokenDecimals(networkId), + ) + return TxFeeState.SingleFeeState( + fee = TxFee( + feeValue = normalFeeValue, + gasLimit = normalFeeGas, + feeFiatFormatted = normalFiatFee, + feeCryptoFormatted = normalCryptoFee, + cryptoSymbol = networkCurrency, + feeType = FeeType.NORMAL, + ), + ) + } + + private suspend fun TransactionFee.toTxFeeState(networkId: String): TxFeeState { + val networkCurrency = userWalletManager.getNetworkCurrency(networkId) + return when (this) { + is TransactionFee.Choosable -> { + val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO + val feePriority = this.priority.amount.value ?: BigDecimal.ZERO + val normalFiatValue = getFormattedFiatFees(networkId, feeNormal)[0] + val priorityFiatValue = getFormattedFiatFees(networkId, feePriority)[0] + val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( + amount = feeNormal, + decimals = transactionManager.getNativeTokenDecimals(networkId), + ) + val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI( + amount = feePriority, + decimals = transactionManager.getNativeTokenDecimals(networkId), + ) + TxFeeState.MultipleFeeState( + normalFee = TxFee( + feeValue = feeNormal, + gasLimit = this.normal.getGasLimit(), + feeFiatFormatted = normalFiatValue, + feeCryptoFormatted = normalCryptoFee, + cryptoSymbol = networkCurrency, + feeType = FeeType.NORMAL, + ), + priorityFee = TxFee( + feeValue = feePriority, + gasLimit = this.priority.getGasLimit(), + feeFiatFormatted = priorityFiatValue, + feeCryptoFormatted = priorityCryptoFee, + cryptoSymbol = networkCurrency, + feeType = FeeType.PRIORITY, + ), + ) + } + is TransactionFee.Single -> { + val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO + val normalFiatValue = getFormattedFiatFees(networkId, feeNormal)[0] + val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( + amount = feeNormal, + decimals = transactionManager.getNativeTokenDecimals(networkId), + ) + TxFeeState.SingleFeeState( + fee = TxFee( + feeValue = this.normal.amount.value ?: BigDecimal.ZERO, + gasLimit = this.normal.getGasLimit(), + feeFiatFormatted = normalFiatValue, + feeCryptoFormatted = normalCryptoFee, + cryptoSymbol = networkCurrency, + feeType = FeeType.NORMAL, + ), + ) + } + } + } + + private fun Fee.getGasLimit(): Int { + return when (this) { + is Fee.Common -> 0 + is Fee.Ethereum -> this.gasLimit.toInt() + } + } + + private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): BigDecimal { + return when (txFeeState) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.SingleFeeState -> txFeeState.fee.feeValue + is TxFeeState.MultipleFeeState -> when (feeType) { + FeeType.NORMAL -> txFeeState.normalFee.feeValue + FeeType.PRIORITY -> txFeeState.priorityFee.feeValue + } + } + } + private fun isBalanceEnough( networkId: String, fromToken: CryptoCurrency, @@ -866,7 +1039,7 @@ internal class SwapInteractorImpl @Inject constructor( } } - override suspend fun checkFeeIsEnough( + private suspend fun checkFeeIsEnough( fee: BigDecimal?, spendAmount: SwapAmount, networkId: String, @@ -938,10 +1111,7 @@ internal class SwapInteractorImpl @Inject constructor( } companion object { - private const val DEFAULT_SLIPPAGE = 2 - @Suppress("UnusedPrivateMember") - private const val INCREASE_FEE_TO_CHECK_ENOUGH_PERCENT = 1.0 // if need to increase fee when check isEnough private const val INCREASE_GAS_LIMIT_BY = 112 // 12% private const val INFINITY_SYMBOL = "∞" diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index bdcfad48f9..4be138a37f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -5,6 +5,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* @@ -36,6 +37,8 @@ class SwapDomainModule { @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, quotesRepository: QuotesRepository, + walletManagersFacade: WalletManagersFacade, + coroutineDispatcherProvider: CoroutineDispatcherProvider, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -49,6 +52,8 @@ class SwapDomainModule { getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, quotesRepository = quotesRepository, + walletManagersFacade = walletManagersFacade, + dispatcher = coroutineDispatcherProvider, ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt index 30271929e7..89ab7c6754 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt @@ -16,7 +16,7 @@ sealed class ExpressTransactionModel { override val txTo: String, val txFrom: String, val txData: String, - ): ExpressTransactionModel() + ) : ExpressTransactionModel() data class CEX( override val fromAmount: SwapAmount, @@ -25,6 +25,5 @@ sealed class ExpressTransactionModel { override val txTo: String, val externalTxId: String, val externalTxUrl: String, - ): ExpressTransactionModel() - + ) : ExpressTransactionModel() } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt index 5defcf6bf3..01914d161b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt @@ -25,7 +25,7 @@ class AmountFormatter { * @param currency * @return formatted [String] */ - fun formatBigDecimalAmountToUI(amount: BigDecimal, decimals: Int, currency: String): String { + fun formatBigDecimalAmountToUI(amount: BigDecimal, decimals: Int, currency: String? = null): String { return amount.toFormattedCurrencyString(decimals, currency) } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 0dc24830f3..78ccdccca5 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -20,7 +20,8 @@ sealed interface SwapState { isFeeEnough = false, ), val permissionState: PermissionDataState = PermissionDataState.Empty, - val swapDataModel: SwapStateData? = null, + val swapDataModel: SwapDataModel? = null, + val txFee: TxFeeState, val tangemFee: Double, ) : SwapState @@ -62,21 +63,30 @@ data class RequestApproveStateData( val fromTokenAmount: SwapAmount, ) -data class SwapStateData( - val fee: TxFeeState, - val swapModel: SwapDataModel, -) +// data class SwapStateData( +// val fee: TxFeeState, +// val swapModel: SwapDataModel, +// ) -data class TxFeeState( - val normalFee: TxFee, - val priorityFee: TxFee, -) +sealed class TxFeeState { + data class MultipleFeeState( + val normalFee: TxFee, + val priorityFee: TxFee, + ) : TxFeeState() + + data class SingleFeeState( + val fee: TxFee, + ) : TxFeeState() + + object Empty : TxFeeState() +} data class TxFee( val feeValue: BigDecimal, val gasLimit: Int, val feeFiatFormatted: String, val feeCryptoFormatted: String, + val cryptoSymbol: String, val feeType: FeeType, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 82b62f10c7..a685079175 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.feature.swap.domain.models.ui.TxFee +import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState data class SwapStateHolder( @@ -14,13 +15,14 @@ data class SwapStateHolder( val networkCurrency: String, val networkId: String, val blockchainId: String, // not the same as networkId, its local id in app - val fee: FeeState = FeeState.Empty, val warnings: List = emptyList(), val alert: SwapWarning.GenericWarning? = null, val updateInProgress: Boolean = false, val providerState: ProviderState, + val fee: FeeItemState = FeeItemState.Empty, val permissionState: SwapPermissionState = SwapPermissionState.Empty, + val successState: SwapSuccessStateHolder? = null, val selectTokenState: SwapSelectTokenStateHolder? = null, val bottomSheetConfig: TangemBottomSheetConfig? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 7beb38ac1e..76efb65203 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -1,7 +1,5 @@ package com.tangem.feature.swap.models -import com.tangem.core.ui.components.states.Item -import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.domain.models.ui.TxFee data class UiActions( @@ -17,10 +15,9 @@ data class UiActions( val openPermissionBottomSheet: () -> Unit, val hidePermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, - val onSelectItemFee: (Item) -> Unit, // region new actions val onClickFee: () -> Unit, - val onSelectFeeType: (FeeType) -> Unit, + val onSelectFeeType: (TxFee) -> Unit, val onProviderClick: (String) -> Unit, val onProviderSelect: (String) -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt index 82a1008a27..fddf5f557e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseFeeBottomSheetConfig.kt @@ -4,8 +4,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.feature.swap.domain.models.ui.FeeType import kotlinx.collections.immutable.ImmutableList -class ChooseFeeBottomSheetConfig( +data class ChooseFeeBottomSheetConfig( val selectedFee: FeeType, val onSelectFeeType: (FeeType) -> Unit, - val feeItems: ImmutableList, + val feeItems: ImmutableList, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt index beed7112a6..e01a8b8058 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt @@ -1,13 +1,19 @@ package com.tangem.feature.swap.models.states +import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.swap.domain.models.ui.FeeType -data class FeeItemState( - val feeType: FeeType, - val title: String, - val amountCrypto: String, - val symbolCrypto: String, - val amountFiat: String, - val symbolFiat: String, - val onClick: () -> Unit, -) \ No newline at end of file +sealed class FeeItemState { + + data class Content( + val feeType: FeeType, + val title: TextReference, + val amountCrypto: String, + val symbolCrypto: String, + val amountFiatFormatted: String, + val isClickable: Boolean, + val onClick: () -> Unit, + ) : FeeItemState() + + object Empty : FeeItemState() +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index f967baef74..4556f2d785 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -14,6 +14,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig @@ -31,7 +32,7 @@ fun ChooseFeeBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { Column( - modifier = Modifier.background(TangemTheme.colors.background.secondary), + modifier = Modifier.background(TangemTheme.colors.background.primary), ) { Text( text = "Choose fee", // todo replace with strings @@ -73,7 +74,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { content.feeItems.forEach { feeItem -> val isSelected = feeItem.feeType == content.selectedFee val preEllipsizeText = feeItem.amountCrypto - val postEllipsizeText = " ${feeItem.symbolCrypto} (${feeItem.amountFiat} ${feeItem.symbolFiat})" + val postEllipsizeText = " ${feeItem.symbolCrypto} (${feeItem.amountFiatFormatted})" when (feeItem.feeType) { FeeType.NORMAL -> { SelectorRowItem( @@ -103,22 +104,22 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { @Composable private fun ChooseFeeBottomSheetContent_Preview() { val feeItems = listOf( - FeeItemState( + FeeItemState.Content( feeType = FeeType.NORMAL, - title = "Fee", + title = stringReference("Fee"), amountCrypto = "1000", symbolCrypto = "MATIC", - amountFiat = "10", - symbolFiat = "$", + amountFiatFormatted = "(10$)", + isClickable = false, onClick = {}, ), - FeeItemState( + FeeItemState.Content( feeType = FeeType.PRIORITY, - title = "Fee", + title = stringReference("Fee"), amountCrypto = "2000", symbolCrypto = "MATIC", - amountFiat = "20", - symbolFiat = "$", + amountFiatFormatted = "(10$)", + isClickable = false, onClick = {}, ), ).toImmutableList() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt index c1d2976921..f379ae3511 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt @@ -5,35 +5,47 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.rows.SimpleActionRow +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.FeeItemState @Composable -fun FeeItem(state: FeeItemState) { +fun FeeItemBlock(state: FeeItemState) { + if (state is FeeItemState.Content) { + FeeItem(state = state) + } +} + +@Composable +fun FeeItem(state: FeeItemState.Content) { Box( modifier = Modifier .background( color = TangemTheme.colors.background.action, shape = TangemTheme.shapes.roundedCornersXMedium, ) + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) .clickable( onClick = state.onClick, ) .fillMaxWidth() .defaultMinSize(minHeight = TangemTheme.dimens.size68), ) { - val description = "${state.amountCrypto} ${state.symbolCrypto} (${state.amountFiat} ${state.symbolFiat})" + val description = "${state.amountCrypto} ${state.symbolCrypto} (${state.amountFiatFormatted})" SimpleActionRow( modifier = Modifier.padding( start = TangemTheme.dimens.spacing12, top = TangemTheme.dimens.spacing12, ), - title = state.title, + title = state.title.resolveReference(), description = description, + isClickable = state.isClickable, ) } } @@ -41,13 +53,13 @@ fun FeeItem(state: FeeItemState) { @Preview @Composable private fun FeeItemPreview() { - val state = FeeItemState( + val state = FeeItemState.Content( feeType = FeeType.NORMAL, - title = "Fee", + title = stringReference("Fee"), amountCrypto = "1000", symbolCrypto = "MATIC", - amountFiat = "10", - symbolFiat = "$", + amountFiatFormatted = "(1000$)", + isClickable = false, onClick = {}, ) Column { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6d740e3fa6..b6a1ec961e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -5,8 +5,6 @@ import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.Provider import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.components.states.Item -import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -22,9 +20,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* -import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig -import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig -import com.tangem.feature.swap.models.states.ProviderState +import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -76,7 +72,7 @@ internal class StateBuilder( coinId = null, isBalanceHidden = true, ), - fee = FeeState.Loading, + fee = FeeItemState.Empty, networkCurrency = networkInfo.blockchainCurrency, swapButton = SwapButton(enabled = false, loading = true, onClick = {}), onRefresh = {}, @@ -86,7 +82,7 @@ internal class StateBuilder( updateInProgress = true, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, onCancelPermissionBottomSheet = actions.hidePermissionBottomSheet, - providerState = ProviderState.Loading(), + providerState = ProviderState.Empty(), ) } @@ -127,7 +123,7 @@ internal class StateBuilder( ), ), ), - fee = FeeState.Empty, + fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, loading = false, @@ -143,8 +139,8 @@ internal class StateBuilder( toToken: CryptoCurrency, mainTokenId: String, ): SwapStateHolder { - val canSelectSendToken = mainTokenId != fromToken.id.value // TODO look at id matching - val canSelectReceiveToken = mainTokenId != toToken.id.value // TODO look at id matching + val canSelectSendToken = mainTokenId != fromToken.id.value + val canSelectReceiveToken = mainTokenId != toToken.id.value if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder return uiStateHolder.copy( @@ -172,8 +168,9 @@ internal class StateBuilder( balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "", isBalanceHidden = isBalanceHiddenProvider(), ), - fee = FeeState.Loading, + fee = FeeItemState.Empty, swapButton = SwapButton(enabled = false, loading = true, onClick = {}), + providerState = ProviderState.Loading(), permissionState = uiStateHolder.permissionState, updateInProgress = true, ) @@ -185,7 +182,6 @@ internal class StateBuilder( * @param uiStateHolder whole screen state * @param quoteModel data model * @param fromToken token data to swap - * @param onFeeSetup callback for reset fee after auto update * @return updated whole screen state */ @Suppress("LongMethod") @@ -194,7 +190,7 @@ internal class StateBuilder( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, swapProvider: SwapProvider, - onFeeSetup: (TxFee) -> Unit, + selectedFeeType: FeeType, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -221,7 +217,7 @@ internal class StateBuilder( ), ) } - val feeState = createFeeState(quoteModel, uiStateHolder, onFeeSetup) + val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus return uiStateHolder.copy( @@ -254,7 +250,6 @@ internal class StateBuilder( permissionState = convertPermissionState( lastPermissionState = uiStateHolder.permissionState, permissionDataState = quoteModel.permissionState, - feeState = feeState, onGivePermissionClick = actions.onGivePermissionClick, onChangeApproveType = actions.onChangeApproveType, ), @@ -308,7 +303,7 @@ internal class StateBuilder( isBalanceHidden = isBalanceHiddenProvider(), ), warnings = emptyList(), - fee = FeeState.Empty, + fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, loading = false, @@ -397,144 +392,36 @@ internal class StateBuilder( } } - fun updateFeeSelectedItem(uiState: SwapStateHolder, item: Item, isFeeEnough: Boolean): SwapStateHolder { - val newSelectedItem = item.copy( - startText = TextReference.Res(R.string.send_network_fee_title), - ) - val permissionState = uiState.permissionState - val newPermissionState = if (permissionState is SwapPermissionState.ReadyForRequest) { - permissionState.copy( - fee = newSelectedItem.endText, - ) - } else { - permissionState - } - val updateState = when (val fee = uiState.fee) { - is FeeState.Loaded -> { - getUpdatedFeeStateForEnoughFee(uiState, fee, item, newSelectedItem, newPermissionState, isFeeEnough) + private fun createFeeState(txFeeState: TxFeeState, feeType: FeeType): FeeItemState { + val isClickable: Boolean + val fee = when (txFeeState) { + TxFeeState.Empty -> return FeeItemState.Empty + is TxFeeState.SingleFeeState -> { + isClickable = false + txFeeState.fee } - is FeeState.NotEnoughFundsWarning -> { - getUpdatedFeeStateForNotEnoughFee(uiState, fee, item, newSelectedItem, newPermissionState, isFeeEnough) + is TxFeeState.MultipleFeeState -> { + isClickable = true + when (feeType) { + FeeType.NORMAL -> { + txFeeState.normalFee + } + FeeType.PRIORITY -> { + txFeeState.priorityFee + } + } } - else -> uiState } - return if (isFeeEnough) { - updateState.copy( - warnings = uiState.warnings.filterNot { it is SwapWarning.InsufficientFunds }, - ) - } else { - updateState.copy( - warnings = uiState.warnings.plus(SwapWarning.InsufficientFunds), - ) - } - } - @Suppress("LongParameterList") - private fun getUpdatedFeeStateForEnoughFee( - uiState: SwapStateHolder, - fee: FeeState.Loaded, - itemToSelect: Item, - newSelectedItem: Item, - newPermissionState: SwapPermissionState, - isFeeEnough: Boolean, - ): SwapStateHolder { - val newState = fee.state?.copy( - selectedItem = newSelectedItem, - items = selectNewItem(fee.state.items, itemToSelect), + return FeeItemState.Content( + feeType = feeType, + title = stringReference("Fee"), // todo replace with string + amountCrypto = fee.feeCryptoFormatted, + symbolCrypto = fee.cryptoSymbol, + amountFiatFormatted = fee.feeFiatFormatted, + isClickable = isClickable, + onClick = actions.onClickFee, ) - val newFeeState = if (isFeeEnough) { - fee.copy(state = newState) - } else { - FeeState.NotEnoughFundsWarning( - tangemFee = fee.tangemFee, - state = newState, - onSelectItem = fee.onSelectItem, - ) - } - return uiState.copy( - fee = newFeeState, - permissionState = newPermissionState, - swapButton = uiState.swapButton.copy( - enabled = isFeeEnough, - ), - ) - } - - @Suppress("LongParameterList") - private fun getUpdatedFeeStateForNotEnoughFee( - uiState: SwapStateHolder, - fee: FeeState.NotEnoughFundsWarning, - itemToSelect: Item, - newSelectedItem: Item, - newPermissionState: SwapPermissionState, - isFeeEnough: Boolean, - ): SwapStateHolder { - val newState = fee.state?.copy( - selectedItem = newSelectedItem, - items = selectNewItem(fee.state.items, itemToSelect), - ) - val newFeeState = if (isFeeEnough) { - FeeState.Loaded( - tangemFee = fee.tangemFee, - state = newState, - onSelectItem = fee.onSelectItem, - ) - } else { - fee.copy(state = newState) - } - return uiState.copy( - fee = newFeeState, - permissionState = newPermissionState, - swapButton = uiState.swapButton.copy( - enabled = isFeeEnough, - ), - ) - } - - private fun createFeeState( - quoteModel: SwapState.QuotesLoadedState, - uiStateHolder: SwapStateHolder, - onFeeSetup: (TxFee) -> Unit, - ): FeeState { - val previousFeeState = when (val stateFee = uiStateHolder.fee) { - is FeeState.Loaded -> stateFee.state - is FeeState.NotEnoughFundsWarning -> stateFee.state - else -> null - } - val permissionState = quoteModel.permissionState - val feeState = if (permissionState is PermissionDataState.PermissionReadyForRequest) { - permissionState.requestApproveData.fee - } else { - quoteModel.swapDataModel?.fee - } - val selectFeeState = createSelectFeeState( - fee = feeState, - previousState = previousFeeState, - onFeeSetup = onFeeSetup, - ) - return if (quoteModel.preparedSwapConfigState.isFeeEnough) { - FeeState.Loaded( - tangemFee = quoteModel.tangemFee, - state = selectFeeState, - onSelectItem = actions.onSelectItemFee, - ) - } else { - FeeState.NotEnoughFundsWarning( - tangemFee = quoteModel.tangemFee, - state = selectFeeState, - onSelectItem = actions.onSelectItemFee, - ) - } - } - - private fun selectNewItem(items: ImmutableList>, selectItem: Item): ImmutableList> { - return items.map { - if (it.id == selectItem.id) { - it.copy(isSelected = true) - } else { - it.copy(isSelected = false) - } - }.toImmutableList() } fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder { @@ -612,7 +499,6 @@ internal class StateBuilder( private fun convertPermissionState( lastPermissionState: SwapPermissionState, permissionDataState: PermissionDataState, - feeState: FeeState, onGivePermissionClick: () -> Unit, onChangeApproveType: (ApproveType) -> Unit, ): SwapPermissionState { @@ -621,97 +507,33 @@ internal class StateBuilder( } else { ApproveType.UNLIMITED } - val fee = when (feeState) { - is FeeSelectState -> feeState.state?.selectedItem?.endText - else -> null - } return when (permissionDataState) { PermissionDataState.Empty -> SwapPermissionState.Empty PermissionDataState.PermissionFailed -> SwapPermissionState.Empty PermissionDataState.PermissionLoading -> SwapPermissionState.InProgress - is PermissionDataState.PermissionReadyForRequest -> SwapPermissionState.ReadyForRequest( - currency = permissionDataState.currency, - amount = permissionDataState.amount, - approveType = approveType, - walletAddress = getShortAddressValue(permissionDataState.walletAddress), - spenderAddress = getShortAddressValue(permissionDataState.spenderAddress), - fee = fee ?: TextReference.Str(""), - approveButton = ApprovePermissionButton( - enabled = true, - onClick = onGivePermissionClick, - ), - cancelButton = CancelPermissionButton( - enabled = true, - ), - onChangeApproveType = onChangeApproveType, - ) - } - } - - private fun createSelectFeeState( - fee: TxFeeState?, - previousState: SelectableItemsState?, - onFeeSetup: (TxFee) -> Unit, - ): SelectableItemsState? { - if (fee == null) return null - if (previousState == null) { - onFeeSetup.invoke(fee.normalFee) // if there is no previous state, setup normal fee by default - val selectedItemId = 0 - // by default preselect normal - val preselectedItem = Item( - id = selectedItemId, - startText = TextReference.Res(R.string.send_network_fee_title), - endText = TextReference.Str(fee.normalFee.feeCryptoFormatted + fee.normalFee.feeFiatFormatted), - isSelected = true, - data = fee.normalFee, - ) - val feeItems = mutableListOf>() - val normalFeeItem = Item( - id = selectedItemId, - startText = TextReference.Res(R.string.send_fee_picker_normal), - endText = TextReference.Str(fee.normalFee.feeCryptoFormatted + fee.normalFee.feeFiatFormatted), - isSelected = true, - data = fee.normalFee, - ) - val priorityFeeItem = Item( - id = 1, - startText = TextReference.Res(R.string.send_fee_picker_priority), - endText = TextReference.Str(fee.priorityFee.feeCryptoFormatted + fee.priorityFee.feeFiatFormatted), - isSelected = false, - data = fee.priorityFee, - ) - feeItems.add(normalFeeItem) - feeItems.add(priorityFeeItem) - return SelectableItemsState( - selectedItem = preselectedItem, - items = feeItems.toImmutableList(), - ) - } else { - val normalFeeItem = - requireNotNull(previousState.items.firstOrNull()) { "in previousState there are 2 items" } - .copy( - endText = TextReference.Str(fee.normalFee.feeCryptoFormatted + fee.normalFee.feeFiatFormatted), - ) - val priorityFeeItem = - requireNotNull(previousState.items.getOrNull(1)) { "in previousState there are 2 items" } - .copy( - endText = TextReference.Str( - fee.priorityFee.feeCryptoFormatted + fee.priorityFee.feeFiatFormatted, - ), - ) - val selectedEndText = if (normalFeeItem.isSelected) { - onFeeSetup.invoke(fee.normalFee) - normalFeeItem.endText - } else { - onFeeSetup.invoke(fee.priorityFee) - priorityFeeItem.endText + is PermissionDataState.PermissionReadyForRequest -> { + val permissionFee = when (val fee = permissionDataState.requestApproveData.fee) { + TxFeeState.Empty -> error("Fee shouldn't be empty") + is TxFeeState.MultipleFeeState -> fee.priorityFee + is TxFeeState.SingleFeeState -> fee.fee + } + SwapPermissionState.ReadyForRequest( + currency = permissionDataState.currency, + amount = permissionDataState.amount, + approveType = approveType, + walletAddress = getShortAddressValue(permissionDataState.walletAddress), + spenderAddress = getShortAddressValue(permissionDataState.spenderAddress), + fee = TextReference.Str("${permissionFee.feeCryptoFormatted} (${permissionFee.feeFiatFormatted})"), + approveButton = ApprovePermissionButton( + enabled = true, + onClick = onGivePermissionClick, + ), + cancelButton = CancelPermissionButton( + enabled = true, + ), + onChangeApproveType = onChangeApproveType, + ) } - return previousState.copy( - selectedItem = previousState.selectedItem.copy( - endText = selectedEndText, - ), - items = listOf(normalFeeItem, priorityFeeItem).toImmutableList(), - ) } } @@ -775,6 +597,70 @@ internal class StateBuilder( } } + fun showSelectFeeBottomSheet( + uiState: SwapStateHolder, + selectedFee: FeeType, + txFeeState: TxFeeState.MultipleFeeState, + onDismiss: () -> Unit, + ): SwapStateHolder { + val config = ChooseFeeBottomSheetConfig( + selectedFee = selectedFee, + onSelectFeeType = { + val selectedItem = when (it) { + FeeType.NORMAL -> txFeeState.normalFee + FeeType.PRIORITY -> txFeeState.priorityFee + } + actions.onSelectFeeType.invoke(selectedItem) + }, + feeItems = txFeeState.toFeeItemState(), + ) + return uiState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismiss, + content = config, + ), + ) + } + + fun updateSelectedFee(uiState: SwapStateHolder, selectedFee: FeeType): SwapStateHolder { + val config = uiState.bottomSheetConfig?.content as? ChooseFeeBottomSheetConfig + return if (config != null) { + uiState.copy( + bottomSheetConfig = uiState.bottomSheetConfig.copy( + content = config.copy( + selectedFee = selectedFee, + ), + ), + ) + } else { + uiState + } + } + + private fun TxFeeState.MultipleFeeState.toFeeItemState(): ImmutableList { + return listOf( + FeeItemState.Content( + feeType = this.normalFee.feeType, + title = stringReference("Fee"), // todo replace with string + amountCrypto = this.normalFee.feeCryptoFormatted, + symbolCrypto = this.normalFee.cryptoSymbol, + amountFiatFormatted = this.normalFee.feeFiatFormatted, + isClickable = true, + onClick = {}, + ), + FeeItemState.Content( + feeType = this.priorityFee.feeType, + title = stringReference("Fee"), // todo replace with string + amountCrypto = this.priorityFee.feeCryptoFormatted, + symbolCrypto = this.priorityFee.cryptoSymbol, + amountFiatFormatted = this.priorityFee.feeFiatFormatted, + isClickable = true, + onClick = {}, + ), + ).toImmutableList() + } + private fun Map.Entry.convertToProviderState( onProviderSelect: (String) -> Unit, ): ProviderState? { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 95003050c3..a70b6ce6f2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.SwapStateHolder +import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig @@ -32,6 +33,9 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { is ChooseProviderBottomSheetConfig -> { ChooseProviderBottomSheet(config = config) } + is ChooseFeeBottomSheetConfig -> { + ChooseFeeBottomSheet(config = config) + } } } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 59a9ab4cfd..8d81d961db 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -21,20 +21,15 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.components.states.Item -import com.tangem.core.ui.components.states.SelectableItemsState -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.extensions.getActiveIconResByCoinId import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.ui.FeeType -import com.tangem.feature.swap.domain.models.ui.TxFee import com.tangem.feature.swap.models.* +import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R -import kotlinx.collections.immutable.toImmutableList -import java.math.BigDecimal @Suppress("LongMethod") @Composable @@ -77,7 +72,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi ), ) - FeeItem(feeState = state.fee, currency = state.networkCurrency) + FeeItemBlock(state = state.fee) if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings) @@ -249,49 +244,6 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { } } -@Composable -private fun FeeItem(feeState: FeeState, currency: String) { - val titleString = stringResource(id = R.string.send_network_fee_title) - val disclaimer = stringResource(id = R.string.swapping_tangem_fee_disclaimer, "${feeState.tangemFee}%") - when (feeState) { - is FeeState.Loaded -> { - if (feeState.state != null) { - SelectableInfoCard( - state = feeState.state, - disclaimer = disclaimer, - onSelect = feeState.onSelectItem, - ) - } - } - FeeState.Loading -> { - SmallInfoCardWithDisclaimer( - startText = titleString, - endText = "", - disclaimer = disclaimer, - isLoading = true, - ) - } - is FeeState.NotEnoughFundsWarning -> { - if (feeState.state != null) { - SelectableInfoCardWithWarning( - state = feeState.state, - warningText = stringResource( - id = R.string.swapping_not_enough_funds_for_fee, - currency, - currency, - ), - disclaimer = disclaimer, - onSelect = feeState.onSelectItem, - ) - } - } - is FeeState.Empty -> { - // show nothing - // SmallInfoCard(startText = titleString, endText = "") - } - } -} - @Composable private fun SwapWarnings(warnings: List) { Column( @@ -400,58 +352,18 @@ private val receiveCard = SwapCardState.SwapCardData( isBalanceHidden = false, ) -val stateSelectable = SelectableItemsState( - selectedItem = Item( - 0, - TextReference.Str("Balance"), - TextReference.Str("0.4405434 BTC"), - true, - TxFee( - feeValue = BigDecimal.ZERO, - gasLimit = 0, - feeFiatFormatted = "", - feeCryptoFormatted = "", - feeType = FeeType.NORMAL, - ), - ), - items = listOf( - Item( - 0, - TextReference.Str("Normal"), - TextReference.Str("0.4405434 BTC"), - true, - TxFee( - feeValue = BigDecimal.ZERO, - gasLimit = 0, - feeFiatFormatted = "", - feeCryptoFormatted = "", - feeType = FeeType.NORMAL, - ), - ), - Item( - 1, - TextReference.Str("Priority"), - TextReference.Str("0.46 BTC"), - false, - TxFee( - feeValue = BigDecimal.ZERO, - gasLimit = 0, - feeFiatFormatted = "", - feeCryptoFormatted = "", - feeType = FeeType.NORMAL, - ), - ), - ).toImmutableList(), -) - private val state = SwapStateHolder( networkId = "ethereum", sendCardData = sendCard, receiveCardData = receiveCard, - fee = FeeState.Loaded( - tangemFee = 0.0, - state = stateSelectable, - onSelectItem = {}, + fee = FeeItemState.Content( + feeType = FeeType.NORMAL, + title = stringReference("Fee"), + amountCrypto = "100", + symbolCrypto = "1000", + amountFiatFormatted = "(100)", + isClickable = true, + onClick = {}, ), warnings = listOf( SwapWarning.PermissionNeeded( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index a1da108e5f..191d36f5de 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -1,25 +1,29 @@ package com.tangem.feature.swap.viewmodels import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.feature.swap.domain.models.domain.Currency +import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider -import com.tangem.feature.swap.domain.models.ui.* +import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData +import com.tangem.feature.swap.domain.models.ui.SwapState +import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress +import com.tangem.feature.swap.domain.models.ui.TxFee data class SwapProcessDataState( // Initial network id val networkId: String, - @Deprecated("used in old swap mechanism") - val fromCurrency: Currency? = null, - @Deprecated("used in old swap mechanism") - val toCurrency: Currency? = null, val fromCryptoCurrency: CryptoCurrencyStatus? = null, val toCryptoCurrency: CryptoCurrencyStatus? = null, // Amount from input val amount: String? = null, val approveDataModel: RequestApproveStateData? = null, - val swapDataModel: SwapStateData? = null, - val selectedFee: TxFee? = null, + val swapDataModel: SwapDataModel? = null, + val selectedFee: TxFee? = null, // todo val tokensDataState: TokensDataStateExpress? = null, val selectedProvider: SwapProvider? = null, val lastLoadedSwapStates: Map = emptyMap(), -) \ No newline at end of file +) { + + fun getCurrentLoadedSwapState(): SwapState.QuotesLoadedState? { + return lastLoadedSwapStates[selectedProvider] as? SwapState.QuotesLoadedState + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index c70fd5c2a2..41287a8bc6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -18,6 +18,7 @@ import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.domain.PermissionOptions +import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* @@ -271,17 +272,14 @@ internal class SwapViewModel @Inject constructor( private fun setupLoadedState(provider: SwapProvider, state: SwapState, fromToken: CryptoCurrencyStatus) { when (state) { is SwapState.QuotesLoadedState -> { - fillDataState(state.permissionState, state.swapDataModel) + fillLoadedDataState(state, state.permissionState, state.swapDataModel) uiState = stateBuilder.createQuotesLoadedState( uiStateHolder = uiState, quoteModel = state, fromToken = fromToken.currency, swapProvider = provider, - ) { updatedFee -> - dataState = dataState.copy( - selectedFee = updatedFee, - ) - } + selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, + ) } is SwapState.EmptyAmountState -> { uiState = stateBuilder.createQuotesEmptyAmountState( @@ -305,7 +303,11 @@ internal class SwapViewModel @Inject constructor( return state.entries.first { it.key == selectedSwapProvider }.toPair() } - private fun fillDataState(permissionState: PermissionDataState, swapDataModel: SwapStateData?) { + private fun fillLoadedDataState( + state: SwapState.QuotesLoadedState, + permissionState: PermissionDataState, + swapDataModel: SwapDataModel?, + ) { dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) { dataState.copy( approveDataModel = permissionState.requestApproveData, @@ -313,11 +315,24 @@ internal class SwapViewModel @Inject constructor( } else { dataState.copy( swapDataModel = swapDataModel, - selectedFee = swapDataModel?.fee?.normalFee, + selectedFee = selectDefaultFee(state), ) } } + private fun selectDefaultFee(state: SwapState.QuotesLoadedState): TxFee? { + return dataState.selectedFee + ?: when (val txFee = state.txFee) { + TxFeeState.Empty -> null + is TxFeeState.MultipleFeeState -> { + txFee.normalFee + } + is TxFeeState.SingleFeeState -> { + txFee.fee + } + } + } + private fun onSwapClick() { singleTaskScheduler.cancelTask() uiState = stateBuilder.createSwapInProgressState(uiState) @@ -326,7 +341,7 @@ internal class SwapViewModel @Inject constructor( swapInteractor.onSwap( exchangeProviderType = requireNotNull(dataState.selectedProvider?.type), networkId = dataState.networkId, - swapStateData = requireNotNull(dataState.swapDataModel), + swapData = requireNotNull(dataState.swapDataModel), currencyToSend = requireNotNull(dataState.fromCryptoCurrency?.currency), currencyToGet = requireNotNull(dataState.toCryptoCurrency?.currency), amountToSwap = requireNotNull(dataState.amount), @@ -539,8 +554,8 @@ internal class SwapViewModel @Inject constructor( onAmountChanged = { onAmountChanged(it) }, onSwapClick = { onSwapClick() - val sendTokenSymbol = dataState.fromCurrency?.symbol - val receiveTokenSymbol = dataState.toCurrency?.symbol + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol if (sendTokenSymbol != null && receiveTokenSymbol != null) { analyticsEventHandler.send( SwapEvents.ButtonSwapClicked( @@ -552,8 +567,8 @@ internal class SwapViewModel @Inject constructor( }, onGivePermissionClick = { givePermissionsToSwap() - val sendTokenSymbol = dataState.fromCurrency?.symbol - val receiveTokenSymbol = dataState.toCurrency?.symbol + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol if (sendTokenSymbol != null && receiveTokenSymbol != null) { analyticsEventHandler.send( SwapEvents.ButtonPermissionApproveClicked( @@ -590,26 +605,36 @@ internal class SwapViewModel @Inject constructor( onChangeApproveType = { approveType -> uiState = stateBuilder.updateApproveType(uiState, approveType) }, - onSelectItemFee = { feeItem -> - dataState = dataState.copy(selectedFee = feeItem.data) - val spendAmount = dataState.amount?.let { amount -> - val fromToken = dataState.fromCryptoCurrency ?: return@let null - swapInteractor.getSwapAmountForToken(amount, fromToken.currency) - } ?: dataState.approveDataModel?.fromTokenAmount - spendAmount ?: return@UiActions - val fromToken = dataState.fromCryptoCurrency ?: return@UiActions - viewModelScope.launch(dispatchers.io) { - val isFeeEnough = swapInteractor.checkFeeIsEnough( - fee = feeItem.data.feeValue, - spendAmount = spendAmount, - networkId = dataState.networkId, - fromToken = fromToken.currency, - ) - uiState = stateBuilder.updateFeeSelectedItem(uiState, feeItem, isFeeEnough) + onClickFee = { + val selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL + val txFeeState = dataState.getCurrentLoadedSwapState()?.txFee as? TxFeeState.MultipleFeeState + ?: return@UiActions + uiState = stateBuilder.showSelectFeeBottomSheet( + uiState = uiState, + selectedFee = selectedFee, + txFeeState = txFeeState, + ) { + uiState = stateBuilder.dismissBottomSheet(uiState) + } + }, + onSelectFeeType = { + val state = dataState.getCurrentLoadedSwapState() ?: return@UiActions + val fromToken = dataState.fromCryptoCurrency ?: return@UiActions + val amountToSwap = dataState.amount ?: return@UiActions + val selectedProvider = dataState.selectedProvider ?: return@UiActions + uiState = stateBuilder.updateSelectedFee(uiState, it.feeType) + dataState = dataState.copy(selectedFee = it) + viewModelScope.launch(dispatchers.io) { + val updatedState = swapInteractor.updateQuotesStateWithSelectedFee( + state = state, + selectedFee = it.feeType, + fromToken = fromToken, + amountToSwap = amountToSwap, + networkId = dataState.networkId, + ) + setupLoadedState(selectedProvider, updatedState, fromToken) } }, - onClickFee = {}, - onSelectFeeType = {}, onProviderClick = { uiState = stateBuilder.showSelectProviderBottomSheet( uiState = uiState, diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt index be151b6dac..b8b3dfa295 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt @@ -1,7 +1,14 @@ package com.tangem.lib.crypto.models -data class ProxyFees( - val minFee: ProxyFee, - val normalFee: ProxyFee, - val priorityFee: ProxyFee, -) \ No newline at end of file +sealed class ProxyFees { + + data class MultipleFees( + val minFee: ProxyFee, + val normalFee: ProxyFee, + val priorityFee: ProxyFee, + ) : ProxyFees() + + data class SingleFee( + val singleFee: ProxyFee, + ) : ProxyFees() +} \ No newline at end of file From 2a9a80414d770ee9a218e789de1d24fe90762166 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 24 Nov 2023 16:33:38 +0300 Subject: [PATCH 053/139] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 20 +- .../appbar/AppBarWithBackButtonAndIcon.kt | 8 +- .../AmountVisualTransformation.kt | 2 +- .../transactions/TransactionDoneTitle.kt | 87 ++++ .../core/ui/utils/BigDecimalFormatter.kt | 5 + .../res/drawable/ic_empty_in_process_64.xml | 10 + core/ui/src/main/res/drawable/ic_web_24.xml | 13 + data/card/build.gradle.kts | 4 + .../card/DefaultCardSdkConfigRepository.kt | 13 + domain/card/build.gradle.kts | 4 + .../repository/CardSdkConfigRepository.kt | 10 + .../DefaultWalletManagersFacade.kt | 54 +++ .../walletmanager/WalletManagersFacade.kt | 57 ++- domain/tokens/models/build.gradle.kts | 3 + .../domain/tokens/utils/BigDecimalUtils.kt | 24 ++ domain/transaction/build.gradle.kts | 5 + .../transaction/error/SendTransactionError.kt | 10 + .../usecase/SendTransactionUseCase.kt | 61 +++ features/send/impl/build.gradle.kts | 3 + .../features/send/impl/di/SendRouterModule.kt | 5 +- .../send/impl/navigation/DefaultSendRouter.kt | 12 +- .../send/impl/navigation/InnerSendRouter.kt | 9 + .../send/impl/presentation/SendFragment.kt | 10 + .../presentation/domain/SendNotification.kt | 10 + .../presentation/state/SendStateFactory.kt | 12 +- .../impl/presentation/state/SendUiState.kt | 11 +- .../impl/presentation/state/StateRouter.kt | 55 ++- .../SendRecipientMemoFieldConverter.kt | 1 + .../presentation/ui/SendNavigationButtons.kt | 128 ++++-- .../send/impl/presentation/ui/SendScreen.kt | 14 +- .../presentation/ui/amount/AmountField.kt | 2 +- .../ui/fee/SendCustomFeeEthereum.kt | 88 ++-- .../ui/recipient/SendRecipientContent.kt | 32 +- .../ui/recipient/TextFieldWithPaste.kt | 70 ++++ .../presentation/ui/recipient/TextFields.kt | 385 ------------------ .../impl/presentation/ui/send/SendContent.kt | 193 +++++++++ .../viewmodel/MemoVerification.kt | 1 + .../viewmodel/SendClickIntents.kt | 14 + .../presentation/viewmodel/SendViewModel.kt | 128 +++++- 39 files changed, 1074 insertions(+), 499 deletions(-) rename core/ui/src/main/java/com/tangem/core/ui/components/fields/{ => visualtransformations}/AmountVisualTransformation.kt (93%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt create mode 100644 core/ui/src/main/res/drawable/ic_empty_in_process_64.xml create mode 100644 core/ui/src/main/res/drawable/ic_web_24.xml create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 5b68512d77..8dfb19bd16 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -1,6 +1,9 @@ package com.tangem.tap.di.domain +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -15,10 +18,25 @@ internal object TransactionDomainModule { @Provides @ViewModelScoped - fun provideGetUseCase( + fun provideGetFeeUseCase( walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, ): GetFeeUseCase { return GetFeeUseCase(walletManagersFacade, dispatchers) } + + @Provides + @ViewModelScoped + fun provideSendTransactionUseCase( + isDemoCardUseCase: IsDemoCardUseCase, + walletManagersFacade: WalletManagersFacade, + cardSdkConfigRepository: CardSdkConfigRepository, + ): SendTransactionUseCase { + return SendTransactionUseCase( + isDemoCardUseCase = isDemoCardUseCase, + cardSdkConfigRepository = cardSdkConfigRepository, + walletManagersFacade = walletManagersFacade, + + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt index 73509da21f..6151499106 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt @@ -4,10 +4,13 @@ import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,7 +42,10 @@ fun AppBarWithBackButtonAndIcon( contentDescription = null, modifier = Modifier .size(size = TangemTheme.dimens.size24) - .clickable { onBackClick() }, + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { onBackClick() }, tint = TangemTheme.colors.icon.primary1, ) AnimatedContent( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt similarity index 93% rename from core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt index 89e4fa09ce..b3f27e218e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountVisualTransformation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.fields +package com.tangem.core.ui.components.fields.visualtransformations import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt new file mode 100644 index 0000000000..d576eafe66 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt @@ -0,0 +1,87 @@ +package com.tangem.core.ui.components.transactions + +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat + +/** + * Common transaction done screen title + * + * @param titleRes title resource + * @param date transaction timestamp in millis + */ +@Composable +fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.ic_empty_in_process_64), + contentDescription = null, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8) + .size(TangemTheme.dimens.size64), + ) + Text( + text = stringResource(id = titleRes), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing32), + ) + Text( + text = stringResource(id = R.string.send_date_format, date.toDateFormat(), date.toTimeFormat()), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4), + ) + } +} + +// region Previews +@Preview +@Composable +private fun TransactionDoneTitlePreview_Light() { + TangemTheme { + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = 0, + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) + } +} + +@Preview +@Composable +private fun TransactionDoneTitlePreview_Dark() { + TangemTheme(isDark = true) { + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = 0, + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index d8e209f0ad..c64c860e28 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.utils +import com.tangem.domain.tokens.model.CryptoCurrency import java.math.BigDecimal import java.math.RoundingMode import java.text.NumberFormat @@ -24,6 +25,10 @@ object BigDecimalFormatter { return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency" } + fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency): String { + return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals) + } + fun formatFiatAmount( fiatAmount: BigDecimal?, fiatCurrencyCode: String, diff --git a/core/ui/src/main/res/drawable/ic_empty_in_process_64.xml b/core/ui/src/main/res/drawable/ic_empty_in_process_64.xml new file mode 100644 index 0000000000..e75b15979e --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_empty_in_process_64.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_web_24.xml b/core/ui/src/main/res/drawable/ic_web_24.xml new file mode 100644 index 0000000000..69e2fc27d9 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_web_24.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/data/card/build.gradle.kts b/data/card/build.gradle.kts index 6015c1ed3a..872f5ce751 100644 --- a/data/card/build.gradle.kts +++ b/data/card/build.gradle.kts @@ -13,6 +13,10 @@ android { dependencies { implementation(deps.androidx.datastore) + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } + implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt index a093b24ba0..97c5e7dc35 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.card import com.tangem.TangemSdk +import com.tangem.blockchain.common.CommonSigner import com.tangem.common.UserCodeType import com.tangem.common.core.CardIdDisplayFormat import com.tangem.common.core.UserCodeRequestPolicy @@ -57,4 +58,16 @@ internal class DefaultCardSdkConfigRepository( } override fun isAccessCodeSavingEnabled(): Boolean = preferencesDataSource.shouldSaveAccessCodes + + override fun getCommonSigner(cardId: String?) = CommonSigner( + tangemSdk = sdk, + cardId = cardId, + initialMessage = null, + ) + + override fun isLinkedTerminal() = sdk.config.linkedTerminal + + override fun setLinkedTerminal(isLinked: Boolean?) { + sdk.config.linkedTerminal = isLinked + } } \ No newline at end of file diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index 71512156cd..342a410f5a 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -19,4 +19,8 @@ dependencies { implementation(deps.tangem.card.core) + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } + } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt index 1190a2952d..47d13a5a8c 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.card.repository import com.tangem.TangemSdk +import com.tangem.blockchain.common.CommonSigner import com.tangem.domain.models.scan.ProductType /** @@ -28,4 +29,13 @@ interface CardSdkConfigRepository { /** Check if access code saving is enabled */ fun isAccessCodeSavingEnabled(): Boolean + + /** Get common signer by [cardId] */ + fun getCommonSigner(cardId: String?): CommonSigner + + /** Check if linked terminal is enabled */ + fun isLinkedTerminal(): Boolean? + + /** Set linked terminal by [isLinked] */ + fun setLinkedTerminal(isLinked: Boolean?) } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 15010e8b52..df99d53034 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -10,6 +10,7 @@ import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.extensions.Result @@ -35,6 +36,7 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import timber.log.Timber import java.math.BigDecimal +import java.util.EnumSet @Suppress("LargeClass") // FIXME: Move to its own module and make internal @@ -431,6 +433,58 @@ class DefaultWalletManagersFacade( ) } + override suspend fun validateTransaction( + amount: Amount, + fee: Amount?, + userWalletId: UserWalletId, + network: Network, + ): EnumSet? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + return walletManager?.validateTransaction(amount, fee) + } + + override suspend fun createTransaction( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): TransactionData? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + val txData = walletManager?.createTransaction(amount, fee, destination)?.copy( + extras = null, // todo add memo [[REDACTED_JIRA]] + ) + + return txData + } + + override suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ): SimpleResult { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + return (walletManager as TransactionSender).send(txData, signer) + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 8e830e0bdd..f2b89b51a6 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -2,13 +2,13 @@ package com.tangem.domain.walletmanager import arrow.core.Either import com.tangem.blockchain.blockchains.solana.RentProvider -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -19,6 +19,7 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import java.math.BigDecimal +import java.util.EnumSet // TODO: Move to its own module /** @@ -161,4 +162,54 @@ interface WalletManagersFacade { userWalletId: UserWalletId, network: Network, ): Result? + + /** + * Validates transaction + * + * @param amount of transaction + * @param fee of transaction + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun validateTransaction( + amount: Amount, + fee: Amount?, + userWalletId: UserWalletId, + network: Network, + ): EnumSet? + + /** + * Creates transaction [TransactionData] + * + * @param amount of transaction + * @param fee of transaction + * @param memo of transaction optional + * @param destination address + * @param userWalletId selected wallet id + * @param network network of currency + */ + @Suppress("LongParameterList") + suspend fun createTransaction( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + ): TransactionData? + + /** + * Sends transaction + * + * @param txData transaction data + * @param signer card signer + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun sendTransaction( + txData: TransactionData, + signer: CommonSigner, + userWalletId: UserWalletId, + network: Network, + ): SimpleResult } \ No newline at end of file diff --git a/domain/tokens/models/build.gradle.kts b/domain/tokens/models/build.gradle.kts index e9075cefa0..a354fa4700 100644 --- a/domain/tokens/models/build.gradle.kts +++ b/domain/tokens/models/build.gradle.kts @@ -12,4 +12,7 @@ android { dependencies { implementation(projects.domain.txhistory.models) implementation(projects.core.analytics.models) + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt new file mode 100644 index 0000000000..fe93d1d8ec --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/utils/BigDecimalUtils.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.tokens.utils + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Token +import com.tangem.domain.tokens.model.CryptoCurrency +import java.math.BigDecimal + +/** Converts `BigDecimal` [cryptoCurrency] to [Amount] */ +fun BigDecimal.convertToAmount(cryptoCurrency: CryptoCurrency) = Amount( + currencySymbol = cryptoCurrency.symbol, + value = this, + decimals = cryptoCurrency.decimals, + type = when (cryptoCurrency) { + is CryptoCurrency.Coin -> AmountType.Coin + is CryptoCurrency.Token -> AmountType.Token( + token = Token( + symbol = cryptoCurrency.symbol, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) + }, +) \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index 8da795757a..571b6c0f50 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -14,10 +14,15 @@ dependencies { implementation(projects.core.utils) + /** Tangem SDKs */ + implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) + implementation(projects.domain.models) implementation(projects.domain.legacy) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.demo) + implementation(projects.domain.card) } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt new file mode 100644 index 0000000000..26fdceff5a --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.transaction.error + +sealed class SendTransactionError { + + object DemoCardError : SendTransactionError() + + data class DataError(val message: String?) : SendTransactionError() + + data class NetworkError(val message: String?) : SendTransactionError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt new file mode 100644 index 0000000000..0a289bfcdc --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet + +class SendTransactionUseCase( + private val isDemoCardUseCase: IsDemoCardUseCase, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val walletManagersFacade: WalletManagersFacade, +) { + suspend operator fun invoke( + txData: TransactionData, + userWallet: UserWallet, + network: Network, + ): Either { + val signer = cardSdkConfigRepository.getCommonSigner( + userWallet.cardId, + ) + + val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal() + if (userWallet.scanResponse.card.isStart2Coin) { + cardSdkConfigRepository.setLinkedTerminal(false) + } + val sendResult = try { + if (isDemoCardUseCase(cardId = userWallet.cardId)) { + SendTransactionError.DemoCardError.left() + } else { + walletManagersFacade.sendTransaction( + txData = txData, + signer = signer, + userWalletId = userWallet.walletId, + network = network, + ).right() + } + } catch (ex: Exception) { + cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) + SendTransactionError.DataError(ex.message).left() + } + + cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) + return sendResult.fold( + ifRight = { result -> + when (result) { + is SimpleResult.Success -> true.right() + is SimpleResult.Failure -> SendTransactionError.NetworkError(result.error.message).left() + } + }, + ifLeft = { it.left() }, + ) + } +} \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index bb59a8e295..1af2bafaee 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.core.featuretoggles) implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.core.navigation) /** Domain modules */ implementation(projects.domain.models) @@ -58,6 +59,8 @@ dependencies { implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) implementation(projects.domain.transaction) + implementation(projects.domain.card) + implementation(projects.domain.demo) /** Feature modules */ implementation(projects.features.send.api) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt index 8bf19609d8..7f38cacd35 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.di +import com.tangem.core.navigation.ReduxNavController import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.DefaultSendRouter import dagger.Module @@ -17,7 +18,7 @@ internal object SendRouterModule { @Provides @ActivityScoped - fun provideSendRouter(): SendRouter { - return DefaultSendRouter() + fun provideSendRouter(reduxNavController: ReduxNavController): SendRouter { + return DefaultSendRouter(reduxNavController) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 4db1a5a674..c72ca92ae7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -1,9 +1,17 @@ package com.tangem.features.send.impl.navigation import androidx.fragment.app.Fragment -import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.ReduxNavController import com.tangem.features.send.impl.presentation.SendFragment -internal class DefaultSendRouter : SendRouter { +internal class DefaultSendRouter( + private val reduxNavController: ReduxNavController, +) : InnerSendRouter { + override fun getEntryFragment(): Fragment = SendFragment.create() + + override fun openUrl(url: String) { + reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt new file mode 100644 index 0000000000..b7cd7f934a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/InnerSendRouter.kt @@ -0,0 +1,9 @@ +package com.tangem.features.send.impl.navigation + +import com.tangem.features.send.api.navigation.SendRouter + +interface InnerSendRouter : SendRouter { + + /** Open website by [url] */ + fun openUrl(url: String) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 2790caab93..cd47d6c8fe 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -8,6 +8,8 @@ import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.ui.SendScreen import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel @@ -24,12 +26,20 @@ internal class SendFragment : ComposeFragment() { @Inject override lateinit var appThemeModeHolder: AppThemeModeHolder + @Inject + lateinit var router: SendRouter + private val viewModel by viewModels() + private val innerSendRouter: InnerSendRouter + get() = requireNotNull(router as? InnerSendRouter) { + "innerSendRouter should be instance of InnerSendRouter" + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) viewModel.setRouter( + innerSendRouter, StateRouter( fragmentManager = WeakReference(parentFragmentManager), ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt new file mode 100644 index 0000000000..720270d86d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/domain/SendNotification.kt @@ -0,0 +1,10 @@ +package com.tangem.features.send.impl.presentation.domain + +sealed class SendNotification { + + sealed class Info(val message: String) : SendNotification() + + sealed class Critical(val message: String) : SendNotification() + + sealed class Error(val message: String) : SendNotification() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 38583d0676..bae35722ae 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -86,6 +86,7 @@ internal class SendStateFactory( amountState = amountStateConverter.convert(Unit), recipientState = recipientStateConverter.convert(Unit), feeState = feeStateConverter.convert(Unit), + sendState = SendStates.SendState(), ) //endregion @@ -122,15 +123,15 @@ internal class SendStateFactory( val recipientState = state.recipientState ?: return state val isValidMemo = validateMemo( - memo = value, + memo = recipientState.addressTextField.value.value, cryptoCurrency = cryptoCurrencyStatusProvider().currency, ) val isAddressInWallet = isNotAddressInWallet( + address = value, walletAddresses = walletAddressesProvider(), - address = recipientState.addressTextField.value.value, ) val isValidAddress = verifyAddress( - address = recipientState.addressTextField.value.value, + address = value, cryptoCurrency = cryptoCurrencyStatusProvider().currency, ) @@ -138,8 +139,7 @@ internal class SendStateFactory( it.copy( value = value, error = when { - !isValidAddress -> TextReference.Res(R.string.send_recipient_address_error) - !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error) + !isValidAddress || !isAddressInWallet -> TextReference.Res(R.string.send_recipient_address_error) else -> null }, isError = !isValidAddress || !isAddressInWallet, @@ -169,11 +169,9 @@ internal class SendStateFactory( cryptoCurrency = cryptoCurrencyStatusProvider().currency, ) - // todo add memo validation error text recipientState.memoTextField?.update { it.copy( value = value, - error = TextReference.Res(R.string.send_memo_destination_tag_error), isError = !isValidMemo, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index 207e63064d..1d7474d6b4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -23,6 +23,7 @@ internal data class SendUiState( val amountState: SendStates.AmountState? = null, val recipientState: SendStates.RecipientState? = null, val feeState: SendStates.FeeState? = null, + val sendState: SendStates.SendState? = null, val recipientList: MutableStateFlow> = MutableStateFlow(PagingData.empty()), val currentState: MutableStateFlow, ) @@ -65,11 +66,14 @@ internal sealed class SendStates { val receivedAmount: MutableStateFlow = MutableStateFlow(""), ) : SendStates() - // todo [REDACTED_JIRA] /** Send state */ data class SendState( - val isSuccess: Boolean, - ) + override val type: SendUiStateType = SendUiStateType.Send, + val isSending: MutableStateFlow = MutableStateFlow(false), + val isSuccess: MutableStateFlow = MutableStateFlow(false), + val transactionDate: MutableStateFlow = MutableStateFlow(0L), + val txUrl: MutableStateFlow = MutableStateFlow(""), + ) : SendStates() } enum class SendUiStateType { @@ -77,5 +81,4 @@ enum class SendUiStateType { Recipient, Fee, Send, - Done, } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index e50ae05ae8..e92791bba9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -9,28 +9,61 @@ internal class StateRouter( private val fragmentManager: WeakReference, ) { var currentState: MutableStateFlow = MutableStateFlow(SendUiStateType.Amount) + private set + + private var isFromSend: Boolean = false + + fun popBackStack() { + fragmentManager.get()?.popBackStack() + } fun onBackClick() { - fragmentManager.get()?.popBackStack() + if (isFromSend) { + showSend() + } else { + when (currentState.value) { + SendUiStateType.Amount -> popBackStack() + SendUiStateType.Recipient -> showAmount() + SendUiStateType.Fee -> showRecipient() + SendUiStateType.Send -> showFee() + } + } } fun onNextClick() { when (currentState.value) { - SendUiStateType.Amount -> currentState.update { SendUiStateType.Recipient } - SendUiStateType.Recipient -> currentState.update { SendUiStateType.Fee } - SendUiStateType.Fee -> currentState.update { SendUiStateType.Send } - SendUiStateType.Send -> currentState.update { SendUiStateType.Done } - SendUiStateType.Done -> onBackClick() + SendUiStateType.Amount -> showRecipient() + SendUiStateType.Recipient -> showFee() + SendUiStateType.Fee -> showSend() + SendUiStateType.Send -> onBackClick() } } fun onPrevClick() { when (currentState.value) { - SendUiStateType.Amount -> onBackClick() - SendUiStateType.Recipient -> currentState.update { SendUiStateType.Amount } - SendUiStateType.Fee -> currentState.update { SendUiStateType.Recipient } - SendUiStateType.Send -> currentState.update { SendUiStateType.Fee } - SendUiStateType.Done -> onBackClick() + SendUiStateType.Amount -> popBackStack() + SendUiStateType.Recipient -> showAmount() + SendUiStateType.Fee -> showRecipient() + SendUiStateType.Send -> popBackStack() } } + + fun showAmount(isFromSend: Boolean = false) { + this.isFromSend = isFromSend + currentState.update { SendUiStateType.Amount } + } + + fun showRecipient(isFromSend: Boolean = false) { + this.isFromSend = isFromSend + currentState.update { SendUiStateType.Recipient } + } + + fun showFee(isFromSend: Boolean = false) { + this.isFromSend = isFromSend + currentState.update { SendUiStateType.Fee } + } + + fun showSend() { + currentState.update { SendUiStateType.Send } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt index d1890038cb..a1c0bd5603 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt @@ -45,6 +45,7 @@ internal class SendRecipientMemoFieldConverter( ), placeholder = TextReference.Res(R.string.send_optional_field), label = TextReference.Res(value), + error = TextReference.Res(R.string.send_memo_destination_tag_error), ), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 30ca727485..e7c0df4249 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -1,23 +1,30 @@ package com.tangem.features.send.impl.presentation.ui +import androidx.annotation.StringRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.R -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.* +import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType @@ -64,16 +71,16 @@ private fun SendSecondaryNavigationButton(uiState: SendUiState) { @Composable private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier = Modifier) { - val currentState = uiState.currentState.collectAsState() + val currentState = uiState.currentState.collectAsStateWithLifecycle() + val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle()?.value ?: false + val isSending = uiState.sendState?.isSending?.collectAsStateWithLifecycle()?.value ?: false + val txUrl = uiState.sendState?.txUrl?.collectAsStateWithLifecycle()?.value.orEmpty() - val buttonTextId = when (currentState.value) { - SendUiStateType.Amount, - SendUiStateType.Recipient, - SendUiStateType.Fee, - -> R.string.common_next - SendUiStateType.Send -> R.string.common_send - else -> R.string.common_close - } + val (buttonTextId, buttonClick) = getButtonData( + currentState = currentState, + isSuccess = isSuccess, + uiState = uiState, + ) val isButtonEnabled = when (currentState.value) { SendUiStateType.Amount -> uiState.amountState?.isPrimaryButtonEnabled ?: false @@ -86,19 +93,92 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState, modifier: Modifier label = "Update send screen state", modifier = modifier, ) { textId -> - if (currentState.value == SendUiStateType.Send) { - PrimaryButtonIconEnd( - text = stringResource(textId), - iconResId = R.drawable.ic_tangem_24, - enabled = isButtonEnabled, - onClick = uiState.clickIntents::onNextClick, - ) - } else { - PrimaryButton( - text = stringResource(textId), - enabled = isButtonEnabled, - onClick = uiState.clickIntents::onNextClick, - ) + when { + currentState.value == SendUiStateType.Send && !isSuccess -> { + PrimaryButtonIconEnd( + text = stringResource(textId), + iconResId = R.drawable.ic_tangem_24, + enabled = isButtonEnabled, + onClick = buttonClick, + showProgress = isSending, + ) + } + currentState.value == SendUiStateType.Send && isSuccess -> { + PrimaryButtonsDone( + textRes = textId, + txUrl = txUrl, + onExploreClick = { uiState.clickIntents.onExploreClick(txUrl) }, + onDoneClick = buttonClick, + modifier = Modifier, + ) + } + else -> { + PrimaryButton( + text = stringResource(textId), + enabled = isButtonEnabled, + onClick = buttonClick, + ) + } } } +} + +@Composable +private fun PrimaryButtonsDone( + @StringRes textRes: Int, + txUrl: String, + onExploreClick: () -> Unit, + onDoneClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hapticFeedback = LocalHapticFeedback.current + val context = LocalContext.current + + Column(modifier = modifier) { + if (txUrl.isNotBlank()) { + Row { + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_explore), + iconResId = R.drawable.ic_web_24, + onClick = onExploreClick, + modifier = Modifier.weight(1f), + ) + SpacerW12() + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_share), + iconResId = R.drawable.ic_share_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + context.shareText(txUrl) + }, + modifier = Modifier.weight(1f), + ) + } + SpacerH12() + } + PrimaryButton( + text = stringResource(id = textRes), + enabled = true, + onClick = onDoneClick, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +private fun getButtonData( + uiState: SendUiState, + currentState: State, + isSuccess: Boolean, +): Pair Unit> { + return when (currentState.value) { + SendUiStateType.Amount, + SendUiStateType.Recipient, + SendUiStateType.Fee, + -> R.string.common_next to uiState.clickIntents::onNextClick + SendUiStateType.Send -> if (isSuccess) { + R.string.common_close + } else { + R.string.common_send + } to uiState.clickIntents::onSendClick + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index c8f289df54..1c73c25a04 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -22,11 +22,13 @@ import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent +import com.tangem.features.send.impl.presentation.ui.send.SendContent @Composable internal fun SendScreen(uiState: SendUiState) { val currentState = uiState.currentState.collectAsStateWithLifecycle() - BackHandler { uiState.clickIntents.onPrevClick() } + val isSuccess = uiState.sendState?.isSuccess?.collectAsStateWithLifecycle() + BackHandler { uiState.clickIntents.onBackClick() } Column( modifier = Modifier .fillMaxSize() @@ -36,12 +38,10 @@ internal fun SendScreen(uiState: SendUiState) { horizontalAlignment = Alignment.CenterHorizontally, ) { val titleRes = when (currentState.value) { - SendUiStateType.Amount, - SendUiStateType.Send, - -> R.string.common_send + SendUiStateType.Amount -> R.string.common_send SendUiStateType.Recipient -> R.string.send_recipient SendUiStateType.Fee -> R.string.common_fee_selector_title - SendUiStateType.Done -> null + SendUiStateType.Send -> if (isSuccess?.value == false) R.string.common_send else null } val iconRes = when (currentState.value) { SendUiStateType.Amount, @@ -52,7 +52,7 @@ internal fun SendScreen(uiState: SendUiState) { AppBarWithBackButtonAndIcon( text = titleRes?.let { stringResource(it) }, - onBackClick = uiState.clickIntents::onBackClick, + onBackClick = uiState.clickIntents::popBackStack, onIconClick = uiState.clickIntents::onQrCodeScanClick, backIconRes = R.drawable.ic_close_24, iconRes = iconRes, @@ -94,7 +94,7 @@ private fun SendScreenContent( uiState.feeState, uiState.clickIntents, ) - else -> { /* [REDACTED_TODO_COMMENT]*/ } + SendUiStateType.Send -> SendContent(uiState) } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt index 2fedacc5c1..f6471c020d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.style.TextAlign -import com.tangem.core.ui.components.fields.AmountVisualTransformation +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt index a96d8c5f1b..98b24927f0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFeeEthereum.kt @@ -1,17 +1,21 @@ package com.tangem.features.send.impl.presentation.ui.fee +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.fields.AmountVisualTransformation +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.components.inputrow.InputRowEnter +import com.tangem.core.ui.components.inputrow.InputRowEnterInfo +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import com.tangem.features.send.impl.presentation.ui.recipient.TextFieldWithInfo +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer private const val ETHEREUM_UNIT = "GWEI" @@ -22,42 +26,66 @@ internal fun SendCustomFeeEthereum( symbol: String, modifier: Modifier = Modifier, ) { - val fee = customValues.value[0] - val gasPrice = customValues.value[1] - val gasLimit = customValues.value[2] - if (selectedFee == FeeType.CUSTOM && customValues.value.isNotEmpty()) { + val fee = customValues.value[0] + val gasPrice = customValues.value[1] + val gasLimit = customValues.value[2] + Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = modifier, ) { - TextFieldWithInfo( - value = fee.value, - label = stringResource(R.string.send_max_fee), + FooterContainer( footer = stringResource(R.string.send_max_fee_footer), - info = fee.label, - visualTransformation = AmountVisualTransformation(symbol), - keyboardOptions = fee.keyboardOptions, - onValueChange = fee.onValueChange, - isSingleLine = true, - ) - TextFieldWithInfo( - value = gasPrice.value, - label = stringResource(R.string.send_gas_price), + ) { + InputRowEnterInfo( + text = fee.value, + title = TextReference.Res(R.string.send_max_fee), + info = fee.label, + visualTransformation = AmountVisualTransformation(symbol), + keyboardOptions = fee.keyboardOptions, + onValueChange = fee.onValueChange, + isSingleLine = true, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + FooterContainer( footer = stringResource(R.string.send_gas_price_footer), - onValueChange = gasPrice.onValueChange, - visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT), - keyboardOptions = fee.keyboardOptions, - isSingleLine = true, - ) - TextFieldWithInfo( - value = gasLimit.value, - label = stringResource(R.string.send_gas_limit), + ) { + InputRowEnter( + text = gasPrice.value, + title = TextReference.Res(R.string.send_gas_price), + onValueChange = gasPrice.onValueChange, + visualTransformation = AmountVisualTransformation(ETHEREUM_UNIT), + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + FooterContainer( footer = stringResource(R.string.send_gas_limit_footer), - onValueChange = gasLimit.onValueChange, - keyboardOptions = fee.keyboardOptions, - isSingleLine = true, - ) + ) { + InputRowEnter( + text = gasLimit.value, + title = TextReference.Res(R.string.send_gas_limit), + onValueChange = gasLimit.onValueChange, + keyboardOptions = fee.keyboardOptions, + isSingleLine = true, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 30563155b4..24319091e1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -18,11 +18,13 @@ import androidx.compose.ui.res.stringResource import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey +import com.tangem.core.ui.components.inputrow.InputRowRecipient import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" @@ -45,18 +47,26 @@ internal fun SendRecipientContent( .padding(horizontal = TangemTheme.dimens.spacing16), ) { item(key = ADDRESS_FIELD_KEY) { - TextFieldWithPasteAndIcon( - value = address.value, - label = address.label, - placeholder = address.placeholder, + FooterContainer( footer = stringResource(R.string.send_recipient_address_footer, uiState.network), - onValueChange = address.onValueChange, - onPasteClick = clickIntents::onRecipientAddressValueChange, - singleLine = true, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing4), - isError = address.isError, - error = address.error, - ) + ) { + InputRowRecipient( + value = address.value, + title = address.label, + placeholder = address.placeholder, + onValueChange = address.onValueChange, + onPasteClick = clickIntents::onRecipientAddressValueChange, + singleLine = true, + isError = address.isError, + error = address.error, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } } memo?.let { memoField -> item(key = MEMO_FIELD_KEY) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt new file mode 100644 index 0000000000..8b3c83bfb0 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -0,0 +1,70 @@ +package com.tangem.features.send.impl.presentation.ui.recipient + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.components.inputrow.inner.PasteButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.ui.common.FooterContainer + +@Composable +internal fun TextFieldWithPaste( + value: String, + placeholder: TextReference, + label: TextReference, + onValueChange: (String) -> Unit, + onPasteClick: (String) -> Unit, + modifier: Modifier = Modifier, + footer: String? = null, + error: TextReference? = null, + isError: Boolean = false, +) { + val (title, color) = if (isError && error != null) { + error to TangemTheme.colors.text.warning + } else { + label to TangemTheme.colors.text.secondary + } + FooterContainer(modifier, footer) { + Row( + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.body2, + color = color, + ) + SimpleTextField( + value = value, + placeholder = placeholder, + onValueChange = onValueChange, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing6), + ) + } + PasteButton( + isPasteButtonVisible = value.isBlank(), + onClick = onPasteClick, + modifier = Modifier + .align(CenterVertically) + .padding(end = TangemTheme.dimens.spacing16), + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt deleted file mode 100644 index f2a792f03e..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFields.kt +++ /dev/null @@ -1,385 +0,0 @@ -package com.tangem.features.send.impl.presentation.ui.recipient - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.CenterVertically -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.icons.identicon.IdentIcon -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.ui.common.FooterContainer - -@Composable -internal fun TextFieldWithPasteAndIcon( - value: String, - placeholder: TextReference, - label: TextReference, - onValueChange: (String) -> Unit, - onPasteClick: (String) -> Unit, - modifier: Modifier = Modifier, - footer: String? = null, - singleLine: Boolean = false, - error: TextReference? = null, - isError: Boolean = false, -) { - val (title, color) = if (isError && error != null) { - error to TangemTheme.colors.text.warning - } else { - label to TangemTheme.colors.text.secondary - } - FooterContainer(modifier, footer) { - Column( - modifier = Modifier - .fillMaxWidth() - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) { - Text( - text = title.resolveReference(), - style = TangemTheme.typography.body2, - color = color, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing12, - ), - ) - Row { - IdentIcon( - address = value, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing8, - bottom = TangemTheme.dimens.spacing10, - ) - .clip(RoundedCornerShape(TangemTheme.dimens.radius20)) - .size(TangemTheme.dimens.size40) - .background(TangemTheme.colors.background.tertiary), - ) - SimpleTextField( - value = value, - placeholder = placeholder, - onValueChange = onValueChange, - singleLine = singleLine, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing8, - bottom = TangemTheme.dimens.spacing10, - ) - .weight(1f) - .align(CenterVertically), - ) - PasteButton( - isPasteButtonVisible = value.isBlank(), - onClick = onPasteClick, - modifier = Modifier - .align(CenterVertically) - .padding( - start = TangemTheme.dimens.spacing4, - end = TangemTheme.dimens.spacing16, - ), - ) - } - } - } -} - -@Composable -internal fun TextFieldWithPaste( - value: String, - placeholder: TextReference, - label: TextReference, - onValueChange: (String) -> Unit, - onPasteClick: (String) -> Unit, - modifier: Modifier = Modifier, - footer: String? = null, - error: TextReference? = null, - isError: Boolean = false, -) { - val (title, color) = if (isError && error != null) { - error to TangemTheme.colors.text.warning - } else { - label to TangemTheme.colors.text.secondary - } - FooterContainer(modifier, footer) { - Row( - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), - ) { - Column( - modifier = Modifier - .weight(1f) - .padding(TangemTheme.dimens.spacing12), - ) { - Text( - text = title.resolveReference(), - style = TangemTheme.typography.body2, - color = color, - ) - SimpleTextField( - value = value, - placeholder = placeholder, - onValueChange = onValueChange, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing6), - ) - } - PasteButton( - isPasteButtonVisible = value.isBlank(), - onClick = onPasteClick, - modifier = Modifier - .align(CenterVertically) - .padding(end = TangemTheme.dimens.spacing16), - ) - } - } -} - -@Composable -internal fun TextFieldWithInfo( - value: String, - label: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - info: TextReference? = null, - footer: String? = null, - isSingleLine: Boolean = false, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, -) { - FooterContainer( - footer = footer, - footerTopPadding = TangemTheme.dimens.spacing6, - modifier = modifier, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing14, - ), - ) { - Text( - text = label, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - Row { - SimpleTextField( - value = value, - onValueChange = onValueChange, - visualTransformation = visualTransformation, - singleLine = isSingleLine, - keyboardOptions = keyboardOptions, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing6) - .weight(1f), - ) - info?.let { - Text( - text = it.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing8) - .align(Alignment.Bottom), - ) - } - } - } - } -} - -@Composable -private fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { - val clipboardManager = LocalClipboardManager.current - val hapticFeedback = LocalHapticFeedback.current - - if (isPasteButtonVisible) { - Box(modifier = modifier) { - Text( - text = "Paste", - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary2, - modifier = Modifier - .background( - color = TangemTheme.colors.button.primary, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding( - horizontal = TangemTheme.dimens.spacing10, - vertical = TangemTheme.dimens.spacing2, - ) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(radius = TangemTheme.dimens.radius8), - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onClick( - clipboardManager - .getText() - ?.toString() - .orEmpty(), - ) - }, - ), - ) - } - } else { - Icon( - painter = painterResource(id = R.drawable.ic_close_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = stringResource(R.string.common_close), - modifier = modifier - .size(TangemTheme.dimens.size20) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(radius = TangemTheme.dimens.radius10), - onClick = { onClick("") }, - ), - ) - } -} - -@Composable -private fun SimpleTextField( - value: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - placeholder: TextReference? = null, - singleLine: Boolean = false, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, -) { - val focusRequester = remember { FocusRequester() } - BasicTextField( - value = value, - onValueChange = onValueChange, - textStyle = TangemTheme.typography.body2.copy(color = TangemTheme.colors.text.primary1), - cursorBrush = SolidColor(TangemTheme.colors.text.primary1), - singleLine = singleLine, - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - decorationBox = { textValue -> - Box { - if (value.isBlank() && placeholder != null) { - Text( - text = placeholder.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.disabled, - modifier = Modifier, - ) - } - textValue() - } - }, - modifier = modifier - .focusRequester(focusRequester), - ) -} - -//region preview -@Preview -@Composable -private fun TextFieldPreview_Light() { - TangemTheme { - Column { - TextFieldWithPaste( - value = "", - label = TextReference.Res(R.string.send_recipient), - placeholder = TextReference.Res(R.string.send_enter_address_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithPasteAndIcon( - value = "", - label = TextReference.Res(R.string.send_extras_hint_memo), - placeholder = TextReference.Res(R.string.send_optional_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithInfo( - value = "Text", - label = stringResource(R.string.send_extras_hint_memo), - info = TextReference.Res(R.string.send_optional_field), - footer = stringResource(R.string.send_max_fee), - onValueChange = {}, - ) - } - } -} - -@Preview -@Composable -private fun TextFieldPreview_Dark() { - TangemTheme(isDark = true) { - Column { - TextFieldWithPaste( - value = "", - label = TextReference.Res(R.string.send_recipient), - placeholder = TextReference.Res(R.string.send_enter_address_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithPasteAndIcon( - value = "", - label = TextReference.Res(R.string.send_extras_hint_memo), - placeholder = TextReference.Res(R.string.send_optional_field), - onValueChange = {}, - onPasteClick = {}, - ) - SpacerH8() - TextFieldWithInfo( - value = "Text", - label = stringResource(R.string.send_extras_hint_memo), - info = TextReference.Res(R.string.send_optional_field), - footer = stringResource(R.string.send_max_fee), - onValueChange = {}, - ) - } - } -} -//endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt new file mode 100644 index 0000000000..037913ef69 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -0,0 +1,193 @@ +package com.tangem.features.send.impl.presentation.ui.send + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.toBigDecimalOrDefault +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.components.inputrow.InputRowImage +import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType + +@Suppress("LongMethod") +@Composable +internal fun SendContent(uiState: SendUiState) { + val amountState = uiState.amountState ?: return + val recipientState = uiState.recipientState ?: return + val feeState = uiState.feeState ?: return + val sendState = uiState.sendState ?: return + + val isSuccess = sendState.isSuccess.collectAsStateWithLifecycle() + val timestamp = sendState.transactionDate.collectAsStateWithLifecycle() + + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AnimatedVisibility(visible = isSuccess.value) { + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = timestamp.value, + ) + } + AnimatedVisibility(visible = !isSuccess.value) { + FromWallet( + walletName = amountState.walletName, + walletBalance = amountState.walletBalance, + ) + } + AmountBlock( + amountState = amountState, + isSuccess = isSuccess, + onClick = uiState.clickIntents::showAmount, + ) + RecipientBlock( + recipientState = recipientState, + isSuccess = isSuccess, + onClick = uiState.clickIntents::showRecipient, + ) + FeeBlock( + feeState = feeState, + isSuccess = isSuccess, + onClick = uiState.clickIntents::showFee, + ) + } +} + +@Composable +private fun FromWallet(walletName: String, walletBalance: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.button.disabled) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = buildAnnotatedString { + append(stringResource(R.string.send_from_wallet_android)) + append(" ") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(walletName) + } + }, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = walletBalance, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing8, + ), + ) + } +} + +@Composable +private fun AmountBlock(amountState: SendStates.AmountState, isSuccess: State, onClick: () -> Unit) { + val amount = amountState.amountTextField.collectAsStateWithLifecycle() + + val cryptoAmount = formatCryptoAmount( + cryptoCurrency = amountState.cryptoCurrencyStatus.currency, + cryptoAmount = amount.value.value.toBigDecimalOrDefault(), + ) + val fiatAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = amount.value.fiatValue.toBigDecimalOrDefault(), + fiatCurrencyCode = amountState.appCurrency.code, + fiatCurrencySymbol = amountState.appCurrency.symbol, + ) + InputRowImage( + title = TextReference.Res(R.string.send_amount_label), + subtitle = TextReference.Str(cryptoAmount), + caption = TextReference.Str(fiatAmount), + tokenIconState = amountState.tokenIconState, + showNetworkIcon = true, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isSuccess.value) { onClick() }, + ) +} + +@Composable +private fun RecipientBlock(recipientState: SendStates.RecipientState, isSuccess: State, onClick: () -> Unit) { + val address = recipientState.addressTextField.collectAsStateWithLifecycle() + val memo = recipientState.memoTextField?.collectAsStateWithLifecycle() + + Column( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isSuccess.value) { onClick() }, + ) { + val showMemo = memo != null && memo.value.value.isNotBlank() + InputRowRecipientDefault( + title = TextReference.Res(R.string.send_recipient), + value = address.value.value, + showDivider = showMemo, + ) + if (showMemo) { + InputRowDefault( + title = TextReference.Res(R.string.send_extras_hint_memo), + text = TextReference.Str(memo?.value?.value.orEmpty()), + ) + } + } +} + +@Composable +private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: State, onClick: () -> Unit) { + val feeSelector = + feeState.feeSelectorState.collectAsStateWithLifecycle().value as? FeeSelectorState.Content ?: return + val customValue = feeSelector.customValues.collectAsStateWithLifecycle().value.getOrNull(0) + + val feeValue = formatCryptoAmount( + cryptoCurrency = feeState.cryptoCurrencyStatus.currency, + cryptoAmount = when (val selectedFee = feeSelector.fees) { + is TransactionFee.Single -> selectedFee.normal.amount.value + is TransactionFee.Choosable -> when (feeSelector.selectedFee) { + FeeType.SLOW -> selectedFee.minimum.amount.value + FeeType.MARKET -> selectedFee.normal.amount.value + FeeType.FAST -> selectedFee.priority.amount.value + FeeType.CUSTOM -> customValue?.value.toBigDecimalOrDefault() + } + }, + ) + InputRowDefault( + title = TextReference.Res(R.string.send_network_fee_title), + text = TextReference.Str(feeValue), + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isSuccess.value) { onClick() }, + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt index a27ce13f23..ae24d705fd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt @@ -7,6 +7,7 @@ import java.math.BigInteger internal fun validateMemo(memo: String, cryptoCurrency: CryptoCurrency?): Boolean { if (cryptoCurrency == null) return false + if (memo.isEmpty()) return true return when (cryptoCurrency.network.id.value) { Blockchain.XRP.id -> { val tag = memo.toLongOrNull() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 35df0a6614..d29ab95bee 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -4,6 +4,8 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeType interface SendClickIntents { + fun popBackStack() + fun onBackClick() fun onNextClick() @@ -33,4 +35,16 @@ interface SendClickIntents { fun onSubtractSelect(value: Boolean) // endregion + + // region Send + fun onSendClick() + + fun showAmount() + + fun showRecipient() + + fun showFee() + + fun onExploreClick(txUrl: String) + // endregion } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 02f2caefc6..4fdc321d9f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -9,6 +9,7 @@ import arrow.core.getOrElse import com.tangem.blockchain.blockchains.xrp.XrpAddressService import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.Provider import com.tangem.core.ui.utils.BigDecimalFormatter @@ -18,8 +19,11 @@ import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -28,6 +32,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.SendStateFactory import com.tangem.features.send.impl.presentation.state.SendUiState @@ -48,7 +53,7 @@ import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LongParameterList") +@Suppress("LongParameterList", "TooManyFunctions", "LargeClass") @HiltViewModel internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, @@ -60,6 +65,8 @@ internal class SendViewModel @Inject constructor( private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val getFeeUseCase: GetFeeUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val walletManagersFacade: WalletManagersFacade, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { @@ -73,7 +80,8 @@ internal class SendViewModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() - private var innerRouter: StateRouter by Delegates.notNull() + private var inneRrouter: InnerSendRouter by Delegates.notNull() + private var stateRouter: StateRouter by Delegates.notNull() private val stateFactory = SendStateFactory( clickIntents = this, @@ -102,9 +110,10 @@ internal class SendViewModel @Inject constructor( getFee() } - fun setRouter(router: StateRouter) { - innerRouter = router - uiState = uiState.copy(currentState = router.currentState) + fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) { + inneRrouter = router + this.stateRouter = stateRouter + uiState = uiState.copy(currentState = stateRouter.currentState) } private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) { @@ -271,9 +280,10 @@ internal class SendViewModel @Inject constructor( } // region screen state navigation - override fun onBackClick() = innerRouter.onBackClick() - override fun onNextClick() = innerRouter.onNextClick() - override fun onPrevClick() = innerRouter.onPrevClick() + override fun popBackStack() = stateRouter.popBackStack() + override fun onBackClick() = stateRouter.onBackClick() + override fun onNextClick() = stateRouter.onNextClick() + override fun onPrevClick() = stateRouter.onPrevClick() override fun onQrCodeScanClick() { // TODO Add QR code scanning @@ -314,7 +324,7 @@ internal class SendViewModel @Inject constructor( } private fun checkIfXrpAddressValue(value: String): Boolean { - if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.first() == XRP_X_ADDRESS) { + if (cryptoCurrency.network.id.value == Blockchain.XRP.id && value.firstOrNull() == XRP_X_ADDRESS) { viewModelScope.launch(dispatchers.io) { val result = XrpAddressService.decodeXAddress(value) onRecipientAddressValueChange(result?.address.orEmpty()) @@ -382,8 +392,108 @@ internal class SendViewModel @Inject constructor( } //endregion + // region send state clicks + override fun onSendClick() { + val sendState = uiState.sendState ?: return + + if (sendState.isSuccess.value) popBackStack() + sendState.isSending.update { true } + viewModelScope.launch(dispatchers.io) { + verifyAndSendTransaction() + } + } + + private suspend fun verifyAndSendTransaction() { + val sendState = uiState.sendState ?: return + val amount = uiState.amountState?.amountTextField?.value ?: return + val recipient = uiState.recipientState?.addressTextField?.value ?: return + val feeState = uiState.feeState?.feeSelectorState?.value as? FeeSelectorState.Content ?: return + val memo = uiState.recipientState?.memoTextField?.value + val fee = getFee(feeState) ?: return + + val amountToSend = amount.value.toBigDecimal().convertToAmount(cryptoCurrency) + + // todo add notifications [[REDACTED_JIRA]] + // val transactionErrors = walletManagersFacade.validateTransaction( + // amount = amountToSend, + // fee = fee.amount, + // userWalletId = userWalletId, + // network = cryptoCurrency.network, + // ) + + val txData = walletManagersFacade.createTransaction( + amount = amountToSend, + fee = fee, + memo = memo?.value, + destination = recipient.value, + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) ?: return + + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, + network = cryptoCurrency.network, + ).fold( + ifLeft = { + sendState.isSending.update { false } + // todo add notifications [[REDACTED_JIRA]] + }, + ifRight = { + sendState.transactionDate.update { + txData.date?.timeInMillis ?: System.currentTimeMillis() + } + sendState.isSuccess.update { true } + sendState.txUrl.update { + getTxUrl(txData.hash.orEmpty()) + } + }, + ) + } + + private fun getFee(feeState: FeeSelectorState.Content): Fee? { + return when (val selectedFee = feeState.fees) { + is TransactionFee.Choosable -> { + when (feeState.selectedFee) { + FeeType.SLOW -> selectedFee.minimum + FeeType.MARKET -> selectedFee.normal + FeeType.FAST -> selectedFee.priority + FeeType.CUSTOM -> { + val feeAmount = feeState.customValues.value.firstOrNull()?.value + ?.let { BigDecimal(it) } ?: return null + Fee.Common(feeAmount.convertToAmount(cryptoCurrency)) + } + } + } + is TransactionFee.Single -> selectedFee.normal + } + } + + override fun showAmount() = stateRouter.showAmount(isFromSend = true) + + override fun showRecipient() = stateRouter.showRecipient(isFromSend = true) + + override fun showFee() = stateRouter.showFee(isFromSend = true) + + override fun onExploreClick(txUrl: String) = inneRrouter.openUrl(txUrl) + + private fun getTxUrl(hash: String): String { + val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + // TODO: Fix ton tx urls [REDACTED_TASK_KEY] + return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) { + EMPTY + } else { + getExplorerTransactionUrlUseCase( + txHash = hash, + networkId = cryptoCurrency.network.id, + ) + } + } + // endregion + companion object { private const val XRP_X_ADDRESS = 'X' private const val DEFAULT_VALUE = "0.00" + private const val EMPTY = "" } } \ No newline at end of file From d2d7dd6e71e6140740c9cc2237bb845ae87b7c09 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 12:27:10 +0800 Subject: [PATCH 054/139] Updated on 2026-08-14 --- .../presentation/wallet/state/WalletEvent.kt | 12 + .../wallet/state2/WalletScreenState.kt | 187 ++++++++++++++ .../wallet/state2/WalletStateHolderV2.kt | 54 ++++ .../WalletScreenStateTransformer.kt | 8 + .../wallet/ui/WalletEventEffect.kt | 1 + .../wallet/ui/WalletEventEffectV2.kt | 62 +++++ .../presentation/wallet/ui/WalletScreenV2.kt | 243 ++++++++++++++++++ .../wallet/ui/WalletsListEffectsV2.kt | 45 ++++ .../wallet/ui/components/WalletsList.kt | 36 +++ .../ui/components/common/WalletContent.kt | 18 ++ .../components/common/WalletNotifications.kt | 2 +- .../multicurrency/MultiCurrencyContent.kt | 41 +++ .../multicurrency/MultiCurrencyContentItem.kt | 17 ++ .../wallet/ui/utils/LazyListStateExt.kt | 24 ++ .../wallet/ui/utils/ReviewManagerRequester.kt | 56 ++++ .../ui/utils/ScrollOffsetCollectorV2.kt | 49 ++++ .../ui/utils/WalletsScrollPreviewExt.kt | 46 ++++ 17 files changed, 900 insertions(+), 1 deletion(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletScreenState.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletStateHolderV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletScreenStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffectV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreenV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffectsV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollectorV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsScrollPreviewExt.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt index 9c619d9a9c..a269d87651 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt @@ -17,4 +17,16 @@ internal sealed class WalletEvent { data class CopyAddress(val address: String, val toast: TextReference) : WalletEvent() data class RateApp(val onDismissClick: () -> Unit) : WalletEvent() + + data class DemonstrateWalletsScrollPreview(val direction: Direction) : WalletEvent() { + + enum class Direction { + + /** 1 -> 2 */ + LEFT, + + /** 1 <- 2 */ + RIGHT, + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletScreenState.kt new file mode 100644 index 0000000000..740d54798c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletScreenState.kt @@ -0,0 +1,187 @@ +package com.tangem.feature.wallet.presentation.wallet.state2 + +import androidx.paging.PagingData +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.components.* +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import javax.annotation.concurrent.Immutable + +const val NOT_INITIALIZED_WALLET_INDEX = -1 + +internal data class WalletScreenState( + val onBackClick: () -> Unit, + val topBarConfig: WalletTopBarConfig, + val selectedWalletIndex: Int, + val wallets: ImmutableList, + val onWalletChange: (Int) -> Unit, + val event: StateEvent, + val isHidingMode: Boolean, +) + +internal sealed class WalletState { + + abstract val pullToRefreshConfig: WalletPullToRefreshConfig + abstract val walletCardState: WalletCardState + abstract val warnings: ImmutableList + abstract val bottomSheetConfig: TangemBottomSheetConfig? + + sealed class MultiCurrency : WalletState() { + + abstract val tokensListState: WalletTokensListState + abstract val manageTokensButtonConfig: ManageTokensButtonConfig? + + data class Content( + override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val walletCardState: WalletCardState, + override val warnings: ImmutableList, + override val bottomSheetConfig: TangemBottomSheetConfig?, + override val tokensListState: WalletTokensListState, + override val manageTokensButtonConfig: ManageTokensButtonConfig?, + ) : MultiCurrency() + + data class Locked( + override val walletCardState: WalletCardState, + val onUnlockNotificationClick: () -> Unit, + val isBottomSheetShow: Boolean = false, + val onBottomSheetDismiss: () -> Unit = {}, + val onUnlockClick: () -> Unit, + val onScanClick: () -> Unit, + ) : MultiCurrency() { + + override val pullToRefreshConfig: WalletPullToRefreshConfig + get() = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}) + + override val warnings: ImmutableList = persistentListOf( + WalletNotification.UnlockWallets(onUnlockNotificationClick), + ) + + override val bottomSheetConfig = TangemBottomSheetConfig( + isShow = isBottomSheetShow, + onDismissRequest = onBottomSheetDismiss, + content = WalletBottomSheetConfig.UnlockWallets( + onUnlockClick = onUnlockClick, + onScanClick = onScanClick, + ), + ) + + override val tokensListState = WalletTokensListState.ContentState.Locked + override val manageTokensButtonConfig = null + } + } + + sealed class SingleCurrency : WalletState() { + + abstract val buttons: PersistentList + abstract val marketPriceBlockState: MarketPriceBlockState? + abstract val txHistoryState: TxHistoryState + + data class Content( + override val pullToRefreshConfig: WalletPullToRefreshConfig, + override val walletCardState: WalletCardState, + override val warnings: ImmutableList, + override val bottomSheetConfig: TangemBottomSheetConfig?, + override val buttons: PersistentList, + override val marketPriceBlockState: MarketPriceBlockState, + override val txHistoryState: TxHistoryState, + ) : SingleCurrency() + + data class Locked( + override val walletCardState: WalletCardState, + override val buttons: PersistentList, + val onUnlockNotificationClick: () -> Unit, + val isBottomSheetShow: Boolean = false, + val onBottomSheetDismiss: () -> Unit = {}, + val onUnlockClick: () -> Unit, + val onScanClick: () -> Unit, + val onExploreClick: () -> Unit, + ) : SingleCurrency() { + + override val pullToRefreshConfig: WalletPullToRefreshConfig + get() = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}) + + override val warnings: ImmutableList = persistentListOf( + WalletNotification.UnlockWallets(onUnlockNotificationClick), + ) + + override val bottomSheetConfig = TangemBottomSheetConfig( + isShow = isBottomSheetShow, + onDismissRequest = onBottomSheetDismiss, + content = WalletBottomSheetConfig.UnlockWallets( + onUnlockClick = onUnlockClick, + onScanClick = onScanClick, + ), + ) + + override val marketPriceBlockState: MarketPriceBlockState? = null + + override val txHistoryState: TxHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = PagingData.from( + data = listOf( + TxHistoryState.TxHistoryItemState.Title(onExploreClick = onExploreClick), + TxHistoryState.TxHistoryItemState.Transaction( + state = TransactionState.Locked(txHash = "LOCKED_TX_HASH"), + ), + ), + ), + ), + ) + } + } +} + +internal sealed class WalletTokensListState { + + object Empty : WalletTokensListState() + + sealed class ContentState : WalletTokensListState() { + + abstract val items: ImmutableList + abstract val organizeTokensButtonConfig: OrganizeTokensButtonConfig? + + object Loading : ContentState() { + override val items = persistentListOf() + override val organizeTokensButtonConfig = null + } + + data class Content( + override val items: ImmutableList, + override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?, + ) : ContentState() + + object Locked : ContentState() { + override val items = persistentListOf( + TokensListItemState.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)), + TokensListItemState.Token(state = TokenItemState.Locked(id = "Locked#1")), + ) + override val organizeTokensButtonConfig = null + } + } + + data class OrganizeTokensButtonConfig(val isEnabled: Boolean, val onClick: () -> Unit) + + @Immutable + sealed class TokensListItemState { + + abstract val id: Any + + data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemState() + + data class Token(val state: TokenItemState) : TokensListItemState() { + override val id: String = state.id + } + } +} + +internal data class ManageTokensButtonConfig(val onClick: () -> Unit) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletStateHolderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletStateHolderV2.kt new file mode 100644 index 0000000000..c956da9c5d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/WalletStateHolderV2.kt @@ -0,0 +1,54 @@ +package com.tangem.feature.wallet.presentation.wallet.state2 + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.WalletScreenStateTransformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Wallet state holder + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class WalletStateHolderV2 @Inject constructor() { + + val uiState: StateFlow get() = mutableUiState + val value: WalletScreenState get() = uiState.value + + private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) + + fun update(function: (WalletScreenState) -> WalletScreenState) { + mutableUiState.update(function = function) + } + + fun update(transformer: WalletScreenStateTransformer) { + mutableUiState.update(function = transformer::transform) + } + + fun getSelectedWallet(): WalletState { + return with(value) { wallets[selectedWalletIndex] } + } + + fun getSelectedWalletId(): UserWalletId { + return with(value) { wallets[selectedWalletIndex].walletCardState.id } + } + + private fun getInitialState(): WalletScreenState { + return WalletScreenState( + onBackClick = {}, + topBarConfig = WalletTopBarConfig(onDetailsClick = {}), + selectedWalletIndex = NOT_INITIALIZED_WALLET_INDEX, + wallets = persistentListOf(), + onWalletChange = {}, + event = consumedEvent(), + isHidingMode = false, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletScreenStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletScreenStateTransformer.kt new file mode 100644 index 0000000000..05cbc33f27 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletScreenStateTransformer.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal interface WalletScreenStateTransformer { + + fun transform(prevState: WalletScreenState): WalletScreenState +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index a6a26d1828..dbb5f28cfa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -66,6 +66,7 @@ internal fun WalletEventEffect( } .addOnFailureListener(Timber::e) } + is WalletEvent.DemonstrateWalletsScrollPreview -> Unit } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffectV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffectV2.kt new file mode 100644 index 0000000000..73d54f4ddc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffectV2.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import android.widget.Toast +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.ui.utils.ReviewManagerRequester +import com.tangem.feature.wallet.presentation.wallet.ui.utils.animateScrollByIndex +import com.tangem.feature.wallet.presentation.wallet.ui.utils.demonstrateScrolling + +@Suppress("LongParameterList") +@Composable +internal fun WalletEventEffectV2( + walletsListState: LazyListState, + snackbarHostState: SnackbarHostState, + event: StateEvent, + selectedWalletIndex: Int, + onAutoScrollSet: () -> Unit, + onAlertConfigSet: (WalletAlertState) -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val resources = LocalContext.current.resources + val clipboardManager = LocalClipboardManager.current + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is WalletEvent.ChangeWallet -> { + onAutoScrollSet() + walletsListState.animateScrollByIndex(prevIndex = selectedWalletIndex, newIndex = value.index) + } + is WalletEvent.ShowError -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + is WalletEvent.ShowToast -> { + Toast.makeText(context, value.text.resolveReference(resources), Toast.LENGTH_SHORT).show() + } + is WalletEvent.CopyAddress -> { + clipboardManager.setText(AnnotatedString(value.address)) + Toast.makeText(context, value.toast.resolveReference(resources), Toast.LENGTH_SHORT).show() + } + is WalletEvent.ShowAlert -> onAlertConfigSet(value.state) + is WalletEvent.RateApp -> { + ReviewManagerRequester.request(context = context, onDismissClick = value.onDismissClick) + } + is WalletEvent.DemonstrateWalletsScrollPreview -> { + walletsListState.demonstrateScrolling(coroutineScope = coroutineScope, direction = value.direction) + } + } + }, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreenV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreenV2.kt new file mode 100644 index 0000000000..fc946a27f4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreenV2.kt @@ -0,0 +1,243 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet +import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state2.NOT_INITIALIZED_WALLET_INDEX +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet +import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton +import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.controlButtons +import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock +import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun WalletScreenV2(state: WalletScreenState) { + BackHandler(onBack = state.onBackClick) + + // It means that screen is still initializing + if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return + + val walletsListState = rememberLazyListState(initialFirstVisibleItemIndex = state.selectedWalletIndex) + val snackbarHostState = remember(::SnackbarHostState) + val isAutoScroll = remember { mutableStateOf(value = false) } + + WalletContent( + state = state, + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + isAutoScroll = isAutoScroll, + onAutoScrollReset = { isAutoScroll.value = false }, + ) + + var alertConfig by remember { mutableStateOf(value = null) } + + alertConfig?.let { + WalletAlert(state = it, onDismiss = { alertConfig = null }) + } + + WalletEventEffectV2( + event = state.event, + selectedWalletIndex = state.selectedWalletIndex, + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + onAlertConfigSet = { alertConfig = it }, + onAutoScrollSet = { isAutoScroll.value = true }, + ) +} + +@Suppress("LongMethod") +@Composable +private fun WalletContent( + state: WalletScreenState, + walletsListState: LazyListState, + snackbarHostState: SnackbarHostState, + isAutoScroll: State, + onAutoScrollReset: () -> Unit, +) { + var selectedWalletIndex by remember { mutableIntStateOf(state.selectedWalletIndex) } + val selectedWallet = state.wallets[selectedWalletIndex] + + BaseScaffold(state = state, selectedWallet = selectedWallet, snackbarHostState = snackbarHostState) { + val movableItemModifier = Modifier.changeWalletAnimator(walletsListState) + + val lazyTxHistoryItems = (selectedWallet as? WalletState.SingleCurrency)?.let { walletState -> + (walletState.txHistoryState as? TxHistoryState.Content)?.contentItems?.collectAsLazyPagingItems() + } + + val txHistoryItems by remember(selectedWallet.walletCardState.id, lazyTxHistoryItems?.itemCount) { + mutableStateOf(value = lazyTxHistoryItems) + } + + val betweenItemsPadding = TangemTheme.dimens.spacing14 + val horizontalPadding = TangemTheme.dimens.spacing16 + val itemModifier = movableItemModifier + .padding(top = betweenItemsPadding) + .padding(horizontal = horizontalPadding) + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing92, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item( + key = state.wallets.map { it.walletCardState.id }, + contentType = state.wallets.map { it.walletCardState.id }, + ) { + WalletsList( + lazyListState = walletsListState, + wallets = state.wallets.map(WalletState::walletCardState).toImmutableList(), + isBalanceHidden = state.isHidingMode, + ) + } + + (selectedWallet as? WalletState.SingleCurrency)?.let { + controlButtons( + configs = it.buttons, + selectedWalletIndex = selectedWalletIndex, + modifier = movableItemModifier.padding(top = betweenItemsPadding), + ) + } + + notifications(configs = selectedWallet.warnings, modifier = itemModifier) + + (selectedWallet as? WalletState.SingleCurrency)?.let { walletState -> + walletState.marketPriceBlockState?.let { marketPriceBlockState -> + marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier) + } + } + + contentItemsV2( + state = selectedWallet, + txHistoryItems = txHistoryItems, + isBalanceHidden = state.isHidingMode, + modifier = movableItemModifier, + ) + + organizeTokens(state = selectedWallet, itemModifier = itemModifier) + } + + val bottomSheetConfig = selectedWallet.bottomSheetConfig + if (bottomSheetConfig != null) { + when (bottomSheetConfig.content) { + is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig) + is TokenReceiveBottomSheetConfig -> TokenReceiveBottomSheet(config = bottomSheetConfig) + is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig) + is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig) + } + } + + WalletsListEffectsV2( + lazyListState = walletsListState, + selectedWalletIndex = selectedWalletIndex, + onWalletChange = state.onWalletChange, + onSelectedWalletIndexSet = { selectedWalletIndex = it }, + isAutoScroll = isAutoScroll, + onAutoScrollReset = onAutoScrollReset, + ) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun BaseScaffold( + state: WalletScreenState, + selectedWallet: WalletState, + snackbarHostState: SnackbarHostState, + content: @Composable () -> Unit, +) { + Scaffold( + topBar = { WalletTopBar(config = state.topBarConfig) }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + floatingActionButton = { + val manageTokensButtonConfig by remember(state.selectedWalletIndex) { + mutableStateOf( + (state.wallets[state.selectedWalletIndex] as? WalletState.MultiCurrency)?.manageTokensButtonConfig, + ) + } + + manageTokensButtonConfig?.let { ManageTokensButton(onClick = it.onClick) } + }, + floatingActionButtonPosition = FabPosition.Center, + containerColor = TangemTheme.colors.background.secondary, + content = { + val pullRefreshState = rememberPullRefreshState( + refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + onRefresh = selectedWallet.pullToRefreshConfig.onRefresh, + ) + + Box( + modifier = Modifier + .pullRefresh(pullRefreshState) + .padding(it), + ) { + content() + + WalletPullToRefreshIndicator( + isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } + }, + ) +} + +@Composable +private fun ManageTokensButton(onClick: () -> Unit) { + PrimaryButton( + text = stringResource(id = R.string.main_manage_tokens), + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + ) +} + +internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modifier) { + (state as? WalletState.MultiCurrency)?.let { + (state.tokensListState as? WalletTokensListState.ContentState)?.let { + it.organizeTokensButtonConfig?.let { config -> + organizeTokensButton( + modifier = itemModifier, + isEnabled = config.isEnabled, + onClick = config.onClick, + ) + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffectsV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffectsV2.kt new file mode 100644 index 0000000000..d7dc0bb129 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffectsV2.kt @@ -0,0 +1,45 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.snapshotFlow +import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollectorV2 +import com.tangem.feature.wallet.presentation.wallet.ui.utils.WalletsListInteractionsCollector + +@Suppress("LongParameterList") +@Composable +internal fun WalletsListEffectsV2( + lazyListState: LazyListState, + selectedWalletIndex: Int, + onWalletChange: (Int) -> Unit, + onSelectedWalletIndexSet: (Int) -> Unit, + isAutoScroll: State, + onAutoScrollReset: () -> Unit, +) { + LaunchedEffect(key1 = lazyListState, key2 = onWalletChange) { + snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo } + .collect( + collector = ScrollOffsetCollectorV2( + selectedWalletIndex = selectedWalletIndex, + lazyListState = lazyListState, + onWalletChange = { newIndex -> + // Auto scroll must not change wallet + if (isAutoScroll.value) { + onSelectedWalletIndexSet(newIndex) + } else { + onSelectedWalletIndexSet(newIndex) + onWalletChange(newIndex) + } + }, + ), + ) + } + + LaunchedEffect(Unit) { + lazyListState.interactionSource.interactions.collect( + collector = WalletsListInteractionsCollector(onDragStart = onAutoScrollReset), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 7caf718821..ff70bf8486 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -23,8 +23,10 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletCard +import kotlinx.collections.immutable.ImmutableList private const val SHORT_SNAP_ELEMENT_COUNT = 50 @@ -66,6 +68,40 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState } } +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun WalletsList( + lazyListState: LazyListState, + wallets: ImmutableList, + isBalanceHidden: Boolean, +) { + val horizontalCardPadding = TangemTheme.dimens.spacing16 + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val itemWidth by remember(screenWidth) { derivedStateOf { screenWidth - horizontalCardPadding * 2 } } + + LazyRow( + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + state = lazyListState, + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState, itemWidth = itemWidth), + ) { + items( + items = wallets, + key = { it.id.stringValue }, + contentType = { it.id.stringValue }, + ) { state -> + WalletCard( + state = state, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .animateItemPlacement() + .width(itemWidth), + ) + } + } +} + /** * Custom implementation of fling behaviour that overrides 'shortSnapVelocityThreshold'. * Every user's drag action will similar to a short snap diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 13ff628917..8ad776a987 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -9,6 +9,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencySt import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItemsV2 +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState as WalletStateV2 /** * Wallet content @@ -29,4 +31,20 @@ internal fun LazyListScope.contentItems( is WalletMultiCurrencyState -> tokensListItems(state.tokensListState, modifier, isBalanceHidden) is WalletSingleCurrencyState -> txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) } +} + +internal fun LazyListScope.contentItemsV2( + state: WalletStateV2, + txHistoryItems: LazyPagingItems?, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + when (state) { + is WalletStateV2.MultiCurrency -> { + tokensListItemsV2(state.tokensListState, modifier, isBalanceHidden) + } + is WalletStateV2.SingleCurrency -> { + txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index e60ec4b043..9e0f9f5123 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -22,7 +22,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + contentItemsV2( + items = state.items, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + } + WalletTokensListStateV2.Empty -> nonContentItem(modifier = modifier) + } +} + private fun LazyListScope.contentItems( items: ImmutableList, modifier: Modifier = Modifier, @@ -67,6 +86,28 @@ private fun LazyListScope.contentItems( ) } +private fun LazyListScope.contentItemsV2( + items: ImmutableList, + modifier: Modifier = Modifier, + isBalanceHidden: Boolean, +) { + itemsIndexed( + items = items, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { index, item -> + MultiCurrencyContentItem( + state = item, + isBalanceHidden = isBalanceHidden, + modifier = modifier.roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + ), + ) + }, + ) +} + @OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { item( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt index 9678a22d13..48f04fb8ff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.TokensListItemState /** * Multi-currency content item @@ -29,4 +30,20 @@ internal fun MultiCurrencyContentItem( TokenItem(state = state.state, isBalanceHidden = isBalanceHidden, modifier = modifier) } } +} + +@Composable +internal fun MultiCurrencyContentItem( + state: TokensListItemState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + when (state) { + is TokensListItemState.NetworkGroupTitle -> { + NetworkGroupItem(networkName = state.name.resolveReference(), modifier = modifier) + } + is TokensListItemState.Token -> { + TokenItem(state = state.state, isBalanceHidden = isBalanceHidden, modifier = modifier) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt new file mode 100644 index 0000000000..a9ccd192d4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import androidx.compose.animation.core.tween +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.lazy.LazyListLayoutInfo +import androidx.compose.foundation.lazy.LazyListState + +/** + * Animate scroll [LazyListState]. + * + * [LazyListState] method for scroll with animation by index isn't supported custom animation. + * This extension method calculate offset between [prevIndex] and [newIndex], + * and scroll by it with default animation. + */ +internal suspend fun LazyListState.animateScrollByIndex(prevIndex: Int, newIndex: Int) { + animateScrollBy( + value = calculateOffset(layoutInfo, prevIndex, newIndex), + animationSpec = tween(durationMillis = 1000), + ) +} + +private fun calculateOffset(layoutInfo: LazyListLayoutInfo, prevIndex: Int, newIndex: Int): Float { + return layoutInfo.viewportSize.width.times(other = newIndex - prevIndex).toFloat() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt new file mode 100644 index 0000000000..a52e61dd42 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt @@ -0,0 +1,56 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import com.google.android.play.core.review.ReviewInfo +import com.google.android.play.core.review.ReviewManager +import com.google.android.play.core.review.ReviewManagerFactory +import com.google.android.play.core.tasks.Task +import timber.log.Timber + +internal object ReviewManagerRequester { + + fun request(context: Context, onDismissClick: () -> Unit) { + val reviewManager = ReviewManagerFactory.create(context) + val requestTask = reviewManager.requestReviewFlow() + + requestTask + .addOnCompleteListener { + handleOnCompleteRequestTask( + reviewManager = reviewManager, + activity = context.findActivity(), + task = it, + onDismissClick = onDismissClick, + ) + } + .addOnFailureListener(Timber::e) + } + + private fun handleOnCompleteRequestTask( + reviewManager: ReviewManager, + activity: Activity, + task: Task, + onDismissClick: () -> Unit, + ) { + if (task.isSuccessful) { + val reviewFlow = reviewManager.launchReviewFlow(activity, task.result) + reviewFlow + .addOnCompleteListener { resultReviewTask -> + if (!resultReviewTask.isSuccessful) onDismissClick() + } + .addOnFailureListener(Timber::e) + } else { + Timber.e(task.exception) + } + } + + private fun Context.findActivity(): Activity { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + error("Permissions should be called in the context of an Activity") + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollectorV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollectorV2.kt new file mode 100644 index 0000000000..43a4af298b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollectorV2.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import kotlinx.coroutines.flow.FlowCollector +import kotlin.math.abs + +/** + * Flow collector for scroll items tracking. + * If first visible item offset is greater than half item size, then change selected wallet index. + * If last visible item offset is greater than half item size, then change selected wallet index. + * + * @param selectedWalletIndex selected wallet index + * @property lazyListState lazy list state + * @property onWalletChange callback that will be invoked on wallet change + * +[REDACTED_AUTHOR] + */ +internal class ScrollOffsetCollectorV2( + selectedWalletIndex: Int, + private val lazyListState: LazyListState, + private val onWalletChange: (Int) -> Unit, +) : FlowCollector> { + + private val LazyListItemInfo.halfItemSize + get() = size.div(other = 2) + + private var currentIndex = selectedWalletIndex + + override suspend fun emit(value: List) { + if (!lazyListState.isScrollInProgress || value.size <= 1) return + + val firstItem = value.firstOrNull() ?: return + val lastItem = value.lastOrNull() ?: return + + if (abs(firstItem.offset) > firstItem.halfItemSize) { + selectIndex(newIndex = firstItem.index + 1) + } else if (abs(lastItem.offset) > lastItem.halfItemSize) { + selectIndex(newIndex = lastItem.index - 1) + } + } + + private fun selectIndex(newIndex: Int) { + if (currentIndex != newIndex) { + currentIndex = newIndex + onWalletChange(newIndex) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsScrollPreviewExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsScrollPreviewExt.kt new file mode 100644 index 0000000000..3dbb6ccb11 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsScrollPreviewExt.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.gestures.animateScrollBy +import androidx.compose.foundation.lazy.LazyListState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent.DemonstrateWalletsScrollPreview +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +private const val VISIBLE_PART_OF_WALLET_CARD = 0.2f + +internal fun LazyListState.demonstrateScrolling( + coroutineScope: CoroutineScope, + direction: DemonstrateWalletsScrollPreview.Direction, +) { + coroutineScope.launch { + animateScrollBy( + value = calculateOffset(direction = direction, isReverse = false), + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + ) + } + .invokeOnCompletion { + coroutineScope.launch { + animateScrollBy( + value = calculateOffset(direction = direction, isReverse = true), + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow, + ), + ) + } + } +} + +private fun LazyListState.calculateOffset( + direction: DemonstrateWalletsScrollPreview.Direction, + isReverse: Boolean, +): Float { + val sign = when (direction) { + DemonstrateWalletsScrollPreview.Direction.LEFT -> 1 + DemonstrateWalletsScrollPreview.Direction.RIGHT -> -1 + }.times(other = if (isReverse) -1 else 1) + + return layoutInfo.viewportSize.width.toFloat() * VISIBLE_PART_OF_WALLET_CARD * sign +} \ No newline at end of file From 31c82b5331f8ba8438868ee610515841f3152325 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 13:22:59 +0800 Subject: [PATCH 055/139] Updated on 2026-08-14 --- .../tap/di/domain/SettingsDomainModule.kt | 12 ++++++ .../local/preferences/PreferencesKeys.kt | 2 + .../settings/DefaultSettingsRepository.kt | 19 ++++++++++ .../data/settings/di/SettingsDataModule.kt | 2 + .../settings/IsWalletsScrollPreviewEnabled.kt | 15 ++++++++ .../NeverToShowWalletsScrollPreview.kt | 17 +++++++++ .../repositories/SettingsRepository.kt | 4 ++ .../tokens/GetCryptoCurrencyActionsUseCase.kt | 37 ++++++++++--------- .../wallets/usecase/SelectWalletUseCase.kt | 17 ++++++--- 9 files changed, 102 insertions(+), 23 deletions(-) create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/IsWalletsScrollPreviewEnabled.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowWalletsScrollPreview.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 17782946ce..1c596a4b4d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -91,4 +91,16 @@ internal object SettingsDomainModule { ): UpdateBalanceHidingSettingsUseCase { return UpdateBalanceHidingSettingsUseCase(balanceHidingRepository) } + + @Provides + @ViewModelScoped + fun provideSetWalletsScrollPreviewIsShown(settingsRepository: SettingsRepository): NeverToShowWalletsScrollPreview { + return NeverToShowWalletsScrollPreview(settingsRepository = settingsRepository) + } + + @Provides + @ViewModelScoped + fun provideIsWalletsScrollPreviewEnabled(settingsRepository: SettingsRepository): IsWalletsScrollPreviewEnabled { + return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index d81065c147..6eda0886c9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -32,6 +32,8 @@ object PreferencesKeys { val SELECTED_APP_CURRENCY_KEY by lazy { stringPreferencesKey(name = "selectedAppCurrency") } val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") } + + val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 0fed0929d9..8d3c6f18bc 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -1,16 +1,35 @@ package com.tangem.data.settings import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext internal class DefaultSettingsRepository( private val preferencesDataSource: PreferencesDataSource, + private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) : SettingsRepository { override suspend fun shouldShowSaveUserWalletScreen(): Boolean { return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen } } + + override suspend fun isWalletScrollPreviewEnabled(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.WALLETS_SCROLL_PREVIEW_KEY, + default = true, + ) + } + + override suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) { + appPreferencesStore.store( + key = PreferencesKeys.WALLETS_SCROLL_PREVIEW_KEY, + value = isEnabled, + ) + } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 254235cf84..ba83b09d4d 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -21,10 +21,12 @@ internal object SettingsDataModule { @Singleton fun provideSettingsRepository( preferencesDataSource: PreferencesDataSource, + appPreferencesStore: AppPreferencesStore, dispatchers: CoroutineDispatcherProvider, ): SettingsRepository { return DefaultSettingsRepository( preferencesDataSource = preferencesDataSource, + appPreferencesStore = appPreferencesStore, dispatchers = dispatchers, ) } diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsWalletsScrollPreviewEnabled.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsWalletsScrollPreviewEnabled.kt new file mode 100644 index 0000000000..fc45e34aa0 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/IsWalletsScrollPreviewEnabled.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +/** + * Checks if wallets scroll preview is enabled + * + * @property settingsRepository settings repository + * +[REDACTED_AUTHOR] + */ +class IsWalletsScrollPreviewEnabled(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke(): Boolean = settingsRepository.isWalletScrollPreviewEnabled() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowWalletsScrollPreview.kt b/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowWalletsScrollPreview.kt new file mode 100644 index 0000000000..4080a08bc9 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/NeverToShowWalletsScrollPreview.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +/** + * Never to show wallets scroll preview + * + * @property settingsRepository settings repository + * +[REDACTED_AUTHOR] + */ +class NeverToShowWalletsScrollPreview( + private val settingsRepository: SettingsRepository, +) { + + suspend operator fun invoke() = settingsRepository.setWalletScrollPreviewAvailability(isEnabled = false) +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index cdfe2702da..7dd4507554 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -3,4 +3,8 @@ package com.tangem.domain.settings.repositories interface SettingsRepository { suspend fun shouldShowSaveUserWalletScreen(): Boolean + + suspend fun isWalletScrollPreviewEnabled(): Boolean + + suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 55d6d6adf8..d5f89f57f5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -32,10 +32,7 @@ class GetCryptoCurrencyActionsUseCase( ) { @OptIn(ExperimentalCoroutinesApi::class) - suspend operator fun invoke( - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Flow { + operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, @@ -43,19 +40,25 @@ class GetCryptoCurrencyActionsUseCase( userWalletId = userWallet.walletId, ) val networkId = cryptoCurrencyStatus.currency.network.id - val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) - } else if (!userWallet.isMultiCurrency) { - operations.getPrimaryCurrencyStatusFlow() - } else { - operations.getNetworkCoinFlow(networkId, cryptoCurrencyStatus.currency.network.derivationPath) - } - return networkFlow.mapLatest { maybeCoinStatus -> - createTokenActionsState( - userWalletId = userWallet.walletId, - coinStatus = maybeCoinStatus.getOrNull(), - cryptoCurrencyStatus = cryptoCurrencyStatus, - ) + + return flow { + val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) + } else if (!userWallet.isMultiCurrency) { + operations.getPrimaryCurrencyStatusFlow() + } else { + operations.getNetworkCoinFlow(networkId, cryptoCurrencyStatus.currency.network.derivationPath) + } + + val flow = networkFlow.mapLatest { maybeCoinStatus -> + createTokenActionsState( + userWalletId = userWallet.walletId, + coinStatus = maybeCoinStatus.getOrNull(), + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + } + + emitAll(flow) }.flowOn(dispatchers.io) } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index 4c894e93d2..84303de548 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -2,12 +2,13 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either -import com.tangem.common.doOnFailure -import com.tangem.common.doOnSuccess +import arrow.core.right +import com.tangem.common.CompletionResult import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull import com.tangem.domain.wallets.models.SelectWalletError +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId /** @@ -22,16 +23,20 @@ class SelectWalletUseCase( private val reduxStateHolder: ReduxStateHolder, ) { - suspend operator fun invoke(userWalletId: UserWalletId): Either { + suspend operator fun invoke(userWalletId: UserWalletId): Either { return either { val userWalletsListManager = ensureUserWalletListManagerNotNull( walletsStateHolder = walletsStateHolder, raise = { SelectWalletError.DataError }, ) - userWalletsListManager.select(userWalletId) - .doOnFailure { raise(SelectWalletError.UnableToSelectUserWallet) } - .doOnSuccess { reduxStateHolder.onUserWalletSelected(it) } + return when (val result = userWalletsListManager.select(userWalletId)) { + is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet) + is CompletionResult.Success -> { + reduxStateHolder.onUserWalletSelected(result.data) + result.data.right() + } + } } } } \ No newline at end of file From 1e19ce06b8872468d4fe7b6618426494c35c2abb Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 14:58:28 +0200 Subject: [PATCH 056/139] Updated on 2026-08-14 --- .../express/models/request/AssetsRequestBody.kt | 1 - .../api/express/models/response/Asset.kt | 15 --------------- .../repository/DefaultCurrenciesRepository.kt | 1 - .../DefaultMarketCryptoCurrencyRepository.kt | 1 - 4 files changed, 18 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt index ffe4a0a5b0..2d496a94ec 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/request/AssetsRequestBody.kt @@ -4,5 +4,4 @@ import com.squareup.moshi.Json data class AssetsRequestBody( @Json(name = "tokensList") val tokensList: List?, - @Json(name = "onlyActive") val onlyActive: Boolean, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt index dc2fd581fe..87f83ea98f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/Asset.kt @@ -9,21 +9,6 @@ data class Asset( @Json(name = "network") val network: String, - @Json(name = "token") - val token: String, - - @Json(name = "name") - val name: String, - - @Json(name = "symbol") - val symbol: String, - - @Json(name = "decimals") - val decimals: Int, - - @Json(name = "isActive") - val isActive: Boolean, - @Json(name = "exchangeAvailable") val exchangeAvailable: Boolean, ) \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index c7bc498b07..8e5decb644 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -392,7 +392,6 @@ internal class DefaultCurrenciesRepository( val response = tangemExpressApi.getAssets( AssetsRequestBody( tokensList = tokensList, - onlyActive = true, ), ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt index a1b79d9bce..1e3f9eef51 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -15,7 +15,6 @@ class DefaultMarketCryptoCurrencyRepository( return assetsStore.getSyncOrNull(userWalletId)?.find { it.network == cryptoCurrency.network.backendId && - it.token == cryptoCurrency.id.rawCurrencyId && it.contractAddress == contractAddress }?.exchangeAvailable ?: false } From 3416b28b13c0d638efed164dcebd44615051da5d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 15:43:29 +0300 Subject: [PATCH 057/139] Updated on 2026-08-14 --- .../com/tangem/data/tokens/utils/TokensOperations.kt | 4 +--- .../tangem/feature/swap/domain/SwapInteractorImpl.kt | 11 ++++++++--- .../tangem/feature/swap/viewmodels/SwapViewModel.kt | 11 ++++++++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt index 13dcd79a67..54f468860b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -4,7 +4,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.extensions.toCoinId -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency.ID import com.tangem.domain.tokens.model.Network @@ -55,8 +54,7 @@ internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { internal fun getCoinIconUrl(blockchain: Blockchain): String? { val coinId = when (blockchain) { Blockchain.Unknown -> null - Blockchain.TerraV1, Blockchain.TerraV2, Blockchain.Near -> blockchain.toCoinId() - else -> blockchain.toNetworkId() + else -> blockchain.toCoinId() } return coinId?.let(::getTokenIconUrlFromDefaultHost) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 46ace65b78..90d0ca8442 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -785,9 +785,14 @@ internal class SwapInteractorImpl @Inject constructor( userWalletId = userWalletId, cryptoCurrency = fromToken.currency, ).firstOrNull() - txFeeResult?.getOrNull()?.let { txFee -> - return txFee.toTxFeeState(networkId) - } + return txFeeResult?.fold( + ifLeft = { + TxFeeState.Empty + }, + ifRight = { txFee -> + txFee.toTxFeeState(networkId) + } + ) ?: TxFeeState.Empty } return TxFeeState.Empty } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 41287a8bc6..cfd88bd3c6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -295,7 +295,7 @@ internal class SwapViewModel @Inject constructor( } private fun updateLoadedQuotes(state: Map): Pair { - val selectedSwapProvider = dataState.selectedProvider ?: state.keys.first() + val selectedSwapProvider = selectProvider(state) dataState = dataState.copy( selectedProvider = selectedSwapProvider, lastLoadedSwapStates = state, @@ -303,6 +303,15 @@ internal class SwapViewModel @Inject constructor( return state.entries.first { it.key == selectedSwapProvider }.toPair() } + private fun selectProvider(state: Map): SwapProvider { + val currentSelected = dataState.selectedProvider + return if (currentSelected != null && state.keys.contains(currentSelected)) { + currentSelected + } else { + state.keys.first() + } + } + private fun fillLoadedDataState( state: SwapState.QuotesLoadedState, permissionState: PermissionDataState, From 2a5649c94129f3d47d594939499c492837e251c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 18:33:34 +0300 Subject: [PATCH 058/139] Updated on 2026-08-14 --- .../tangem/feature/swap/SwapRepositoryImpl.kt | 5 +- .../swap/converters/QuotesConverter.kt | 15 --- .../feature/swap/domain/SwapInteractorImpl.kt | 94 ++++++++++++++----- .../feature/swap/domain/SwapRepository.kt | 4 + .../domain/models/domain/PermissionOptions.kt | 1 + .../swap/domain/models/domain/QuoteModel.kt | 1 + .../swap/domain/models/ui/SwapState.kt | 1 + .../feature/swap/models/SwapStateHolder.kt | 1 - .../tangem/feature/swap/models/UiActions.kt | 1 - .../states/GivePermissionBottomSheetConfig.kt | 2 +- .../tangem/feature/swap/ui/StateBuilder.kt | 10 +- .../swap/viewmodels/SwapProcessDataState.kt | 2 + .../feature/swap/viewmodels/SwapViewModel.kt | 26 ++--- 13 files changed, 101 insertions(+), 62 deletions(-) delete mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index b5d8b00f73..43010d791d 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -161,6 +161,7 @@ internal class SwapRepositoryImpl @Inject constructor( AggregatedSwapDataModel( dataModel = QuoteModel( toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals), + allowanceContract = response.allowanceContract, ), ) } catch (ex: Exception) { @@ -218,6 +219,7 @@ internal class SwapRepositoryImpl @Inject constructor( derivationPath: String?, tokenDecimalCount: Int, tokenAddress: String, + spenderAddress: String, ): BigDecimal { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val walletManager = walletManagersFacade.getOrCreateWalletManager( @@ -225,7 +227,6 @@ internal class SwapRepositoryImpl @Inject constructor( blockchain = blockchain, derivationPath = derivationPath, ) - val spenderAddress = addressForTrust(networkId) val result = (walletManager as? Approver)?.getAllowance( spenderAddress, @@ -248,6 +249,7 @@ internal class SwapRepositoryImpl @Inject constructor( derivationPath: String?, currency: CryptoCurrency, amount: BigDecimal?, + spenderAddress: String, ): String { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } @@ -256,7 +258,6 @@ internal class SwapRepositoryImpl @Inject constructor( blockchain = blockchain, derivationPath = derivationPath, ) - val spenderAddress = addressForTrust(networkId) return (walletManager as? Approver)?.getApproveData( spenderAddress, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt deleted file mode 100644 index 4352341538..0000000000 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.feature.swap.converters - -import com.tangem.datasource.api.oneinch.models.QuoteResponse -import com.tangem.feature.swap.domain.models.createFromAmountWithOffset -import com.tangem.feature.swap.domain.models.domain.QuoteModel -import com.tangem.utils.converter.Converter - -class QuotesConverter : Converter { - - override fun convert(value: QuoteResponse): QuoteModel { - return QuoteModel( - toTokenAmount = createFromAmountWithOffset(value.toTokenAmount, value.toToken.decimals), - ) - } -} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 90d0ca8442..7b8944fe0a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -197,6 +197,7 @@ internal class SwapInteractorImpl @Inject constructor( networkId = networkId, derivationPath = derivationPath, fromToken = permissionOptions.fromToken, + spenderAddress = permissionOptions.spenderAddress, ) } else { permissionOptions.approveData.approveData @@ -289,8 +290,22 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, isBalanceWithoutFeeEnough: Boolean, ): Pair { + val quotes = repository.findBestQuote( + fromContractAddress = fromToken.currency.getContractAddress(), + fromNetwork = fromToken.currency.network.backendId, + toContractAddress = toToken.currency.getContractAddress(), + toNetwork = toToken.currency.network.backendId, + fromAmount = amount.toStringWithRightOffset(), + fromDecimals = amount.decimals, + providerId = provider.providerId, + rateType = RateType.FLOAT, + ) + val fromTokenAddress = getTokenAddress(fromToken.currency) - val isAllowedToSpend = isAllowedToSpend(networkId, fromToken.currency, amount) + val isAllowedToSpend = quotes.dataModel?.allowanceContract?.let { + isAllowedToSpend(networkId, fromToken.currency, amount, it) + } ?: false + if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) transactionManager.updateWalletManager(networkId, derivationPath) @@ -305,15 +320,16 @@ internal class SwapInteractorImpl @Inject constructor( selectedFee = selectedFee, ) } else { - provider to loadQuoteData( + provider to getQuotesState( exchangeProviderType = ExchangeProviderType.DEX, - networkId = networkId, + quoteDataModel = quotes, amount = amount, - fromTokenStatus = fromToken, - toTokenStatus = toToken, + fromToken = fromToken, + toToken = toToken, + networkId = networkId, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - provider = provider, + providerType = provider.type, selectedFee = selectedFee, ) } @@ -501,16 +517,22 @@ internal class SwapInteractorImpl @Inject constructor( } } - private suspend fun isAllowedToSpend(networkId: String, fromToken: CryptoCurrency, amount: SwapAmount): Boolean { + private suspend fun isAllowedToSpend( + networkId: String, + fromToken: CryptoCurrency, + amount: SwapAmount, + spenderAddress: String, + ): Boolean { if (fromToken is CryptoCurrency.Coin) return true return getSelectedWalletSyncUseCase().fold( ifRight = { userWallet -> val allowance = repository.getAllowance( - userWallet.walletId, - networkId, - derivationPath, - getTokenDecimals(fromToken), - getTokenAddress(fromToken), + userWalletId = userWallet.walletId, + networkId = networkId, + derivationPath = derivationPath, + tokenDecimalCount = getTokenDecimals(fromToken), + tokenAddress = getTokenAddress(fromToken), + spenderAddress = spenderAddress, ) allowance >= amount.value }, @@ -620,6 +642,8 @@ internal class SwapInteractorImpl @Inject constructor( fromToken = fromToken.currency, swapAmount = amount, quotesLoadedState = swapState, + isAllowedToSpend = isAllowedToSpend, + spenderAddress = requireNotNull(quoteModel.allowanceContract) { "Allowance contract is null" }, ) state.copy( preparedSwapConfigState = state.preparedSwapConfigState.copy( @@ -791,19 +815,26 @@ internal class SwapInteractorImpl @Inject constructor( }, ifRight = { txFee -> txFee.toTxFeeState(networkId) - } + }, ) ?: TxFeeState.Empty } return TxFeeState.Empty } - @Suppress("LongParameterList") + @Suppress("LongParameterList", "LongMethod") private suspend fun updatePermissionState( networkId: String, fromToken: CryptoCurrency, swapAmount: SwapAmount, quotesLoadedState: SwapState.QuotesLoadedState, + spenderAddress: String, + isAllowedToSpend: Boolean, ): SwapState.QuotesLoadedState { + if (isAllowedToSpend) { + return quotesLoadedState.copy( + permissionState = PermissionDataState.Empty, + ) + } // if token balance ZERO not show permission state to avoid user to spend money for fee val isTokenZeroBalance = getTokenBalance(networkId, fromToken).value.signum() == 0 if (isTokenZeroBalance) { @@ -822,20 +853,28 @@ internal class SwapInteractorImpl @Inject constructor( derivationPath = derivationPath, fromToken = fromToken, swapAmount = swapAmount, + spenderAddress = spenderAddress, ) - val feeData = transactionManager.getFee( - networkId = networkId, - amountToSend = BigDecimal.ZERO, - currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)), - destinationAddress = getTokenAddress(fromToken), - increaseBy = INCREASE_GAS_LIMIT_BY, - data = transactionData, - derivationPath = derivationPath, - ) - val feeState = when (feeData) { - is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId) - is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId) + val feeData = try { + transactionManager.getFee( + networkId = networkId, + amountToSend = BigDecimal.ZERO, + currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)), + destinationAddress = fromToken.getContractAddress(), + increaseBy = INCREASE_GAS_LIMIT_BY, + data = transactionData, + derivationPath = derivationPath, + ) + } catch (e: Exception) { + Timber.e(e, "Failed to get fee") + null } + val feeState = feeData?.let { + when (feeData) { + is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId) + is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId) + } + } ?: TxFeeState.Empty val fee = when (feeState) { TxFeeState.Empty -> BigDecimal.ZERO is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue @@ -857,6 +896,7 @@ internal class SwapInteractorImpl @Inject constructor( fee = feeState, approveData = transactionData, fromTokenAmount = swapAmount, + spenderAddress = spenderAddress, ), ), preparedSwapConfigState = quotesLoadedState.preparedSwapConfigState.copy( @@ -1089,6 +1129,7 @@ internal class SwapInteractorImpl @Inject constructor( derivationPath: String?, fromToken: CryptoCurrency, swapAmount: SwapAmount? = null, + spenderAddress: String, ): String { return getSelectedWalletSyncUseCase().fold( ifRight = { userWallet -> @@ -1098,6 +1139,7 @@ internal class SwapInteractorImpl @Inject constructor( derivationPath = derivationPath, currency = fromToken, amount = swapAmount?.value, + spenderAddress = spenderAddress, ) }, ifLeft = { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index dd2d72e7df..13650fe4fe 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -39,6 +39,7 @@ interface SwapRepository { */ fun getTangemFee(): Double + @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun getAllowance( userWalletId: UserWalletId, @@ -46,8 +47,10 @@ interface SwapRepository { derivationPath: String?, tokenDecimalCount: Int, tokenAddress: String, + spenderAddress: String, ): BigDecimal + @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun getApproveData( userWalletId: UserWalletId, @@ -55,6 +58,7 @@ interface SwapRepository { derivationPath: String?, currency: CryptoCurrency, amount: BigDecimal?, + spenderAddress: String, ): String @Suppress("LongParameterList") diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt index 88d57a301f..bd1d290190 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt @@ -17,6 +17,7 @@ data class PermissionOptions( val approveData: RequestApproveStateData, val forTokenContractAddress: String, val fromToken: CryptoCurrency, + val spenderAddress: String, val approveType: SwapApproveType, val txFee: TxFee, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt index 5e5d782666..df74da7af3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt @@ -9,4 +9,5 @@ import com.tangem.feature.swap.domain.models.SwapAmount */ data class QuoteModel( val toTokenAmount: SwapAmount, + val allowanceContract: String?, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 78ccdccca5..0c98986dc1 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -61,6 +61,7 @@ data class RequestApproveStateData( val fee: TxFeeState, val approveData: String, val fromTokenAmount: SwapAmount, + val spenderAddress: String, ) // data class SwapStateData( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index a685079175..2335991b5e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -36,7 +36,6 @@ data class SwapStateHolder( val onSuccess: (() -> Unit)? = null, val onMaxAmountSelected: (() -> Unit)? = null, val onShowPermissionBottomSheet: () -> Unit = {}, - val onCancelPermissionBottomSheet: () -> Unit = {}, ) sealed class SwapCardState { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 76efb65203..76844c8cd2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -13,7 +13,6 @@ data class UiActions( val onBackClicked: () -> Unit, val onMaxAmountSelected: () -> Unit, val openPermissionBottomSheet: () -> Unit, - val hidePermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, // region new actions val onClickFee: () -> Unit, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt index 57628ded51..9a8bd7a519 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt @@ -3,7 +3,7 @@ package com.tangem.feature.swap.models.states import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.feature.swap.models.SwapPermissionState -class GivePermissionBottomSheetConfig( +data class GivePermissionBottomSheetConfig( val data: SwapPermissionState.ReadyForRequest, val onCancel: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index b6a1ec961e..6a730aef6e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -81,7 +81,6 @@ internal class StateBuilder( onMaxAmountSelected = actions.onMaxAmountSelected, updateInProgress = true, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, - onCancelPermissionBottomSheet = actions.hidePermissionBottomSheet, providerState = ProviderState.Empty(), ) } @@ -381,10 +380,13 @@ internal class StateBuilder( } fun updateApproveType(uiState: SwapStateHolder, approveType: ApproveType): SwapStateHolder { - return if (uiState.permissionState is SwapPermissionState.ReadyForRequest) { + val config = uiState.bottomSheetConfig?.content as? GivePermissionBottomSheetConfig + return if (config != null) { uiState.copy( - permissionState = uiState.permissionState.copy( - approveType = approveType, + bottomSheetConfig = uiState.bottomSheetConfig.copy( + content = config.copy( + data = config.data.copy(approveType = approveType), + ), ), ) } else { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index 191d36f5de..7b5bd446fa 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -7,6 +7,7 @@ import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress import com.tangem.feature.swap.domain.models.ui.TxFee +import com.tangem.feature.swap.models.ApproveType data class SwapProcessDataState( // Initial network id @@ -16,6 +17,7 @@ data class SwapProcessDataState( // Amount from input val amount: String? = null, val approveDataModel: RequestApproveStateData? = null, + val approveType: ApproveType? = null, val swapDataModel: SwapDataModel? = null, val selectedFee: TxFee? = null, // todo val tokensDataState: TokensDataStateExpress? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index cfd88bd3c6..901ef4daa7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -22,10 +22,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* -import com.tangem.feature.swap.models.SwapPermissionState -import com.tangem.feature.swap.models.SwapStateHolder -import com.tangem.feature.swap.models.UiActions -import com.tangem.feature.swap.models.toDomainApproveType +import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter @@ -320,6 +317,7 @@ internal class SwapViewModel @Inject constructor( dataState = if (permissionState is PermissionDataState.PermissionReadyForRequest) { dataState.copy( approveDataModel = permissionState.requestApproveData, + approveType = dataState.approveType ?: ApproveType.UNLIMITED, ) } else { dataState.copy( @@ -407,12 +405,15 @@ internal class SwapViewModel @Inject constructor( fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) { "dataState.fromCurrency might not be null" }, - approveType = requireNotNull(uiState.permissionState as? SwapPermissionState.ReadyForRequest) { - "uiState.permissionState should be SwapPermissionState.ReadyForRequest" - }.approveType.toDomainApproveType(), + approveType = requireNotNull(dataState.approveType) { + "uiState.permissionState should not be null" + }.toDomainApproveType(), txFee = requireNotNull(dataState.selectedFee) { "dataState.selectedFee shouldn't be null" }, + spenderAddress = requireNotNull(dataState.approveDataModel?.spenderAddress) { + "dataState.approveDataModel.spenderAddress shouldn't be null" + }, ), ) } @@ -604,15 +605,16 @@ internal class SwapViewModel @Inject constructor( openPermissionBottomSheet = { singleTaskScheduler.cancelTask() analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) - uiState = stateBuilder.showPermissionBottomSheet(uiState) { stateBuilder.dismissBottomSheet(uiState) } - }, - hidePermissionBottomSheet = { - startLoadingQuotesFromLastState() - analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked) + uiState = stateBuilder.showPermissionBottomSheet(uiState) { + startLoadingQuotesFromLastState() + analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked) + stateBuilder.dismissBottomSheet(uiState) + } }, onAmountSelected = { onAmountSelected(it) }, onChangeApproveType = { approveType -> uiState = stateBuilder.updateApproveType(uiState, approveType) + dataState = dataState.copy(approveType = approveType) }, onClickFee = { val selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL From 5ce498df75071da85d90664fc89c8f23f725e70c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 18:37:48 +0300 Subject: [PATCH 059/139] Updated on 2026-08-14 --- .../main/res/layout/layout_send_receipt.xml | 2 +- core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-fr/strings.xml | 2 +- core/res/src/main/res/values-it/strings.xml | 2 +- core/res/src/main/res/values-ru/strings.xml | 2 +- .../src/main/res/values-zh-rTW/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 4 +- .../core/ui/components/SuccessScreen.kt | 181 ------------------ .../feature/swap/models/SwapStateHolder.kt | 2 + .../swap/models/SwapSuccessStateHolder.kt | 17 +- .../tangem/feature/swap/ui/StateBuilder.kt | 43 ++++- .../swap/ui/SwapPermissionBottomSheet.kt | 2 +- .../feature/swap/ui/SwapScreenContent.kt | 2 + .../feature/swap/ui/SwapSuccessScreen.kt | 169 ++++++++++++---- .../feature/swap/viewmodels/SwapViewModel.kt | 27 +-- 15 files changed, 222 insertions(+), 237 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/SuccessScreen.kt diff --git a/app/src/main/res/layout/layout_send_receipt.xml b/app/src/main/res/layout/layout_send_receipt.xml index 4a8fab6816..97e7afbb0f 100644 --- a/app/src/main/res/layout/layout_send_receipt.xml +++ b/app/src/main/res/layout/layout_send_receipt.xml @@ -33,7 +33,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" - android:text="@string/send_fee_label" + android:text="@string/common_fee_label" android:textColor="@color/text_tertiary" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 3bc3defd6d..ddbf073872 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -13,6 +13,7 @@ Änderungen speichern Absenden Erfolg + Gebühr Zugangscode Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen. Langes Tippen @@ -40,7 +41,6 @@ Tag Memo inkl. Gebühr - Gebühr Niedrig Normal Priorität diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 097c597e6e..9ac49cb6d8 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -13,6 +13,7 @@ Sauvegarder les modifications Envoyer Avec succès + Commissions Code d\'accès Vous devrez entrer le mot de passe correct avant de scanner la carte Tenez la carte fermement @@ -40,7 +41,6 @@ Tag Memo Inclure les commissions - Commissions Bas Normal Priorité diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index aa5a2b3d47..f673e3bf38 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -13,6 +13,7 @@ Mantieni le modifiche Invia Con successo + Commissione Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto Mantenimento della carta @@ -40,7 +41,6 @@ Tag Memo Includi commissione - Commissione Insufficiente Normale Prioritario diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 3473a16427..77fd07304f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -126,6 +126,7 @@ Отправить Успешно Обмен + Комиссия условия участия Ошибка транзакции Транзакции @@ -423,7 +424,6 @@ Tag Memo Включая комиссию - Комиссия Низкая Нормальная Приоритетная diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 0b9c4e90a9..f595e68773 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -90,6 +90,7 @@ 提交 成功 交換 + 費用 條款和條件 交易 我了解 @@ -294,7 +295,6 @@ Tag Memo 包含費用 - 費用 正常 優先 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index e88c410260..ffc1858e33 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -125,6 +125,7 @@ Submit Success Swap + Fee terms and conditions Transaction failed Transactions @@ -438,7 +439,6 @@ Memo Insufficient funds for transfer Include fee - Fee Low Normal Priority @@ -536,6 +536,8 @@ Unlimited View in Explorer In progress + You swap + You receive Swap Swap of %s to Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/SuccessScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/components/SuccessScreen.kt deleted file mode 100644 index c5c3feabc1..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/SuccessScreen.kt +++ /dev/null @@ -1,181 +0,0 @@ -package com.tangem.core.ui.components - -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.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.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemTheme - -/** - * Screen for showing result - * - * @param resultMessage message to show - * @param title title to show - * @param resultColor color which will tint the round icon of the result - * @param icon icon to show in the middle of the round icon - * @param secondaryButtonIcon icon to show in the secondary button - * @param secondaryButtonText label of the secondary button - * @param onSecondaryButtonClick action on clicking secondary button - * @param onButtonClick action on clicking "Done" button - * - * @see Figma component - */ -@Composable -fun ResultScreenContent( - resultMessage: AnnotatedString, - onButtonClick: () -> Unit, - modifier: Modifier = Modifier, - @StringRes title: Int = R.string.common_success, - resultColor: Color = TangemTheme.colors.icon.accent, - @DrawableRes icon: Int = R.drawable.ic_check_24, - @DrawableRes secondaryButtonIcon: Int? = null, - @StringRes secondaryButtonText: Int? = null, - onSecondaryButtonClick: (() -> Unit)? = null, -) { - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .background(TangemTheme.colors.background.secondary) - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing32, - ), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - SpacerHHalf() - SuccessImage(resultColor = resultColor, icon = icon) - SpacerH50() - Text( - text = stringResource(id = title), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) - SpacerH12() - Text( - text = resultMessage, - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) - SpacerHHalf() - if (onSecondaryButtonClick != null && secondaryButtonText != null) { - SecondaryButtonForResultScreen( - secondaryButtonText = secondaryButtonText, - secondaryButtonIcon = secondaryButtonIcon, - onSecondaryButtonClick = onSecondaryButtonClick, - ) - SpacerH12() - } - PrimaryButton( - text = stringResource(id = R.string.common_close), - modifier = Modifier - .fillMaxWidth(), - onClick = { onButtonClick() }, - ) - } -} - -@Composable -fun SuccessImage(resultColor: Color, @DrawableRes icon: Int) { - Box( - modifier = Modifier - .background( - color = resultColor.copy(alpha = 0.2f), - shape = CircleShape, - ), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .padding(TangemTheme.dimens.spacing24) - .background( - color = resultColor, - shape = CircleShape, - ) - .height(TangemTheme.dimens.size93) - .width(TangemTheme.dimens.size93), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = icon), - contentDescription = null, - tint = TangemTheme.colors.icon.primary2, - modifier = Modifier.size(TangemTheme.dimens.size40), - ) - } - } -} - -@Composable -private fun SecondaryButtonForResultScreen( - @StringRes secondaryButtonText: Int, - onSecondaryButtonClick: () -> Unit, - @DrawableRes secondaryButtonIcon: Int? = null, -) { - if (secondaryButtonIcon != null) { - SecondaryButtonIconStart( - text = stringResource(id = secondaryButtonText), - iconResId = secondaryButtonIcon, - onClick = onSecondaryButtonClick, - modifier = Modifier.fillMaxWidth(), - ) - } else { - SecondaryButton( - text = stringResource(id = secondaryButtonText), - onClick = onSecondaryButtonClick, - modifier = Modifier.fillMaxWidth(), - ) - } -} - -// region preview - -@Composable -private fun SuccessScreenPreview() { - ResultScreenContent( - resultMessage = AnnotatedString("Swap of 1 000 DAI to 1 131,46 MATIC"), - secondaryButtonText = R.string.swapping_success_view_explorer_button_title, - onSecondaryButtonClick = {}, - onButtonClick = {}, - ) -} - -@Preview(showBackground = true) -@Composable -private fun Preview_SuccessScreenContent_InLightTheme() { - TangemTheme(isDark = false) { - SuccessScreenPreview() - } -} - -@Preview(showBackground = true) -@Composable -private fun Preview_SuccessScreenContent_InDarkTheme() { - TangemTheme(isDark = true) { - SuccessScreenPreview() - } -} - -// endregion preview \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index a685079175..21724f8e9e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.TxFee import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState @@ -44,6 +45,7 @@ sealed class SwapCardState { data class SwapCardData( val type: TransactionCardType, val amountEquivalent: String?, + val token: CryptoCurrencyStatus?, val coinId: String?, val amountTextFieldValue: TextFieldValue?, val tokenIconUrl: String?, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt index 56dc3525a8..640c4809d3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt @@ -1,7 +1,20 @@ package com.tangem.feature.swap.models +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.feature.swap.domain.models.domain.SwapProvider + data class SwapSuccessStateHolder( - val fromTokenAmount: String, - val toTokenAmount: String, + val timestamp: Long, + val txUrl: String, + val fee: TextReference, + val rate: TextReference, + val selectedProvider: SwapProvider, + val fromTokenAmount: TextReference, + val toTokenAmount: TextReference, + val fromTokenFiatAmount: TextReference, + val toTokenFiatAmount: TextReference, + val fromTokenIconState: TokenIconState?, + val toTokenIconState: TokenIconState?, val onSecondaryButtonClick: () -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index b6a1ec961e..6e30057199 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.Provider import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -22,6 +23,7 @@ import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.viewmodels.SwapProcessDataState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @@ -37,6 +39,8 @@ internal class StateBuilder( private val appCurrencyProvider: Provider, ) { + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + private val tokensDataConverter = TokensDataConverter( onSearchEntered = actions.onSearchEntered, onTokenSelected = actions.onTokenSelected, @@ -52,6 +56,7 @@ internal class StateBuilder( type = TransactionCardType.SendCard(actions.onAmountChanged, actions.onAmountSelected), amountEquivalent = null, amountTextFieldValue = null, + token = null, tokenIconUrl = initialCurrency.iconUrl, tokenCurrency = initialCurrency.symbol, coinId = initialCurrency.network.backendId, @@ -65,6 +70,7 @@ internal class StateBuilder( amountEquivalent = null, tokenIconUrl = "", tokenCurrency = "", + token = null, amountTextFieldValue = null, canSelectAnotherToken = false, balance = "", @@ -98,6 +104,7 @@ internal class StateBuilder( text = "0", ), amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = fromToken, tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, coinId = uiStateHolder.sendCardData.coinId, isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, @@ -148,6 +155,7 @@ internal class StateBuilder( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = null, + token = uiStateHolder.sendCardData.token, tokenIconUrl = fromToken.iconUrl, tokenCurrency = fromToken.symbol, coinId = fromToken.network.backendId, @@ -160,6 +168,7 @@ internal class StateBuilder( type = TransactionCardType.ReceiveCard(), amountTextFieldValue = null, amountEquivalent = null, + token = uiStateHolder.receiveCardData.token, tokenIconUrl = toToken.iconUrl, tokenCurrency = toToken.symbol, coinId = toToken.network.backendId, @@ -225,6 +234,7 @@ internal class StateBuilder( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = getFormattedFiatAmount(quoteModel.fromTokenInfo.amountFiat), + token = fromCurrencyStatus, tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, coinId = fromCurrencyStatus.currency.network.backendId, isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, @@ -237,6 +247,7 @@ internal class StateBuilder( type = TransactionCardType.ReceiveCard(), amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()), amountEquivalent = getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat), + token = toCurrencyStatus, tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, coinId = toCurrencyStatus.currency.network.backendId, isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, @@ -282,6 +293,7 @@ internal class StateBuilder( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = emptyAmountState.zeroAmountEquivalent, + token = uiStateHolder.sendCardData.token, tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, coinId = uiStateHolder.sendCardData.coinId, isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, @@ -294,6 +306,7 @@ internal class StateBuilder( type = TransactionCardType.ReceiveCard(), amountTextFieldValue = TextFieldValue("0"), amountEquivalent = emptyAmountState.zeroAmountEquivalent, + token = uiStateHolder.receiveCardData.token, tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, coinId = uiStateHolder.receiveCardData.coinId, isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, @@ -434,12 +447,38 @@ internal class StateBuilder( fun createSuccessState( uiState: SwapStateHolder, txState: TxState.TxSent, + dataState: SwapProcessDataState, + txUrl: String, onSecondaryBtnClick: () -> Unit, ): SwapStateHolder { + val fromToken = requireNotNull(uiState.sendCardData as? SwapCardState.SwapCardData) + val toToken = requireNotNull(uiState.receiveCardData as? SwapCardState.SwapCardData) + val fromTokenIconState = fromToken.token?.let(iconStateConverter::convert) + val toTokenIconState = toToken.token?.let(iconStateConverter::convert) + val fee = uiState.fee as? FeeItemState.Content ?: return uiState + val fromCryptoCurrencyStatus = requireNotNull(fromToken.token) + val toCryptoCurrencyStatus = requireNotNull(toToken.token) + val rate = txState.toAmount?.toBigDecimal()?.divide( + txState.fromAmount?.toBigDecimal(), + toCryptoCurrencyStatus.currency.decimals, + RoundingMode.HALF_UP, + ) + val fromCurrencySymbol = fromCryptoCurrencyStatus.currency.symbol + val toCurrencySymbol = toCryptoCurrencyStatus.currency.symbol + return uiState.copy( successState = SwapSuccessStateHolder( - fromTokenAmount = txState.fromAmount ?: "", - toTokenAmount = txState.toAmount ?: "", + timestamp = System.currentTimeMillis(), + txUrl = txUrl, + selectedProvider = requireNotNull(dataState.selectedProvider), + fee = TextReference.Str("${fee.amountCrypto} ${fee.symbolCrypto} (${fee.amountFiatFormatted})"), + rate = TextReference.Str("1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol"), + fromTokenAmount = TextReference.Str("${txState.fromAmount.orEmpty()} ${fromToken.tokenCurrency}}"), + toTokenAmount = TextReference.Str("${txState.toAmount.orEmpty()} ${toToken.tokenCurrency}}"), + fromTokenFiatAmount = TextReference.Str(fromToken.amountEquivalent.orEmpty()), + toTokenFiatAmount = TextReference.Str(toToken.amountEquivalent.orEmpty()), + fromTokenIconState = fromTokenIconState, + toTokenIconState = toTokenIconState, onSecondaryButtonClick = onSecondaryBtnClick, ), ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt index 724c3e44d8..39ecc48a11 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt @@ -272,7 +272,7 @@ private fun DropdownSelector( @Composable private fun FeeItem(fee: String) { InformationItem( - subtitle = stringResource(id = R.string.send_fee_label), + subtitle = stringResource(id = R.string.common_fee_label), value = fee, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 8d81d961db..bf382cbc41 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -336,6 +336,7 @@ private val sendCard = SwapCardState.SwapCardData( canSelectAnotherToken = false, balance = "123", coinId = "", + token = null, isBalanceHidden = false, ) @@ -349,6 +350,7 @@ private val receiveCard = SwapCardState.SwapCardData( canSelectAnotherToken = true, balance = "33333", coinId = "", + token = null, isBalanceHidden = false, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index a05fbdb29b..909239af13 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -1,18 +1,29 @@ package com.tangem.feature.swap.ui -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.ResultScreenContent +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.inputrow.InputRowBestRate +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.components.inputrow.InputRowImage +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType +import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.models.SwapSuccessStateHolder import com.tangem.feature.swap.presentation.R @@ -21,41 +32,117 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) { Scaffold( modifier = Modifier.systemBarsPadding(), content = { padding -> - ResultScreenContent( - resultMessage = makeSuccessMessage( - fromTokenAmount = state.fromTokenAmount, - toTokenAmount = state.toTokenAmount, - ), - resultColor = TangemTheme.colors.icon.attention, - onButtonClick = onBack, - icon = R.drawable.ic_clock_24, - secondaryButtonIcon = R.drawable.ic_arrow_top_right_24, - onSecondaryButtonClick = state.onSecondaryButtonClick, - secondaryButtonText = R.string.swapping_success_view_explorer_button_title, - title = R.string.swapping_success_view_title, - modifier = Modifier.padding(padding), - ) + SwapSuccessScreenContent(padding = padding, state = state) }, topBar = { AppBarWithBackButton( - text = stringResource(R.string.common_swap), onBackClick = onBack, iconRes = R.drawable.ic_close_24, ) }, + bottomBar = { + SwapSuccessScreenButtons( + textRes = R.string.common_close, + txUrl = state.txUrl, + onExploreClick = state.onSecondaryButtonClick, + onDoneClick = onBack, + ) + }, ) } @Composable -private fun makeSuccessMessage(fromTokenAmount: String, toTokenAmount: String): AnnotatedString { - val swapToString = stringResource(id = R.string.swapping_swap_of_to, fromTokenAmount) - val message = "$swapToString\n$toTokenAmount" - return buildAnnotatedString { - append(message) - addStyle( - style = SpanStyle(color = TangemTheme.colors.text.accent), - start = message.indexOf(toTokenAmount), - end = message.length, +private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: PaddingValues) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .background(TangemTheme.colors.background.secondary) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) { + TransactionDoneTitle(titleRes = R.string.swapping_success_view_title, date = 0L) + SpacerH16() + InputRowImage( + title = TextReference.Res(R.string.swapping_success_from_title), + subtitle = state.fromTokenAmount, + caption = state.fromTokenFiatAmount, + tokenIconState = state.fromTokenIconState ?: TokenIconState.Loading, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) + SpacerH16() + InputRowImage( + title = TextReference.Res(R.string.swapping_success_to_title), + subtitle = state.toTokenAmount, + caption = state.toTokenFiatAmount, + tokenIconState = state.toTokenIconState ?: TokenIconState.Loading, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) + SpacerH16() + InputRowBestRate( + imageUrl = state.selectedProvider.imageLarge, + title = TextReference.Str(state.selectedProvider.name), + titleExtra = TextReference.Str(state.selectedProvider.type.name), + subtitle = state.rate, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) + SpacerH16() + InputRowDefault( + title = TextReference.Res(R.string.common_fee_label), + text = state.fee, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) + } +} + +@Composable +private fun SwapSuccessScreenButtons( + @StringRes textRes: Int, + txUrl: String, + onExploreClick: () -> Unit, + onDoneClick: () -> Unit, +) { + val hapticFeedback = LocalHapticFeedback.current + val context = LocalContext.current + + Column( + modifier = Modifier + .background(TangemTheme.colors.background.secondary) + .padding(TangemTheme.dimens.spacing16), + ) { + if (txUrl.isNotBlank()) { + Row { + SecondaryButtonIconStart( + text = stringResource(id = com.tangem.core.ui.R.string.common_explore), + iconResId = com.tangem.core.ui.R.drawable.ic_web_24, + onClick = onExploreClick, + modifier = Modifier.weight(1f), + ) + SpacerW12() + SecondaryButtonIconStart( + text = stringResource(id = com.tangem.core.ui.R.string.common_share), + iconResId = com.tangem.core.ui.R.drawable.ic_share_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + context.shareText(txUrl) + }, + modifier = Modifier.weight(1f), + ) + } + SpacerH12() + } + PrimaryButton( + text = stringResource(id = textRes), + enabled = true, + onClick = onDoneClick, + modifier = Modifier.fillMaxWidth(), ) } } @@ -63,9 +150,25 @@ private fun makeSuccessMessage(fromTokenAmount: String, toTokenAmount: String): // region preview private val state = SwapSuccessStateHolder( - fromTokenAmount = "1 000 DAI", - toTokenAmount = "1 131,46 MATIC", -) {} + timestamp = 0L, + txUrl = "https://www.google.com/#q=nam", + fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), + selectedProvider = SwapProvider( + providerId = "1inch", + rateTypes = listOf(), + name = "1inch", + type = ExchangeProviderType.DEX, + imageLarge = "", + ), + fromTokenAmount = TextReference.Str("1 000 DAI"), + toTokenAmount = TextReference.Str("1 000 MATIC"), + fromTokenFiatAmount = TextReference.Str("1 000 $"), + toTokenFiatAmount = TextReference.Str("1 000 $"), + fromTokenIconState = TokenIconState.Loading, + toTokenIconState = TokenIconState.Loading, + rate = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), + onSecondaryButtonClick = {}, +) @Preview(showBackground = true) @Composable diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 41287a8bc6..76e960107b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -351,17 +351,22 @@ internal class SwapViewModel @Inject constructor( .onSuccess { when (it) { is TxState.TxSent -> { - uiState = stateBuilder.createSuccessState(uiState, it) { - val txHash = it.txAddress - if (txHash.isNotEmpty()) { - swapRouter.openUrl( - blockchainInteractor.getExplorerTransactionLink( - networkId = dataState.networkId, - txAddress = it.txAddress, - ), - ) - } - } + val url = blockchainInteractor.getExplorerTransactionLink( + networkId = dataState.networkId, + txAddress = it.txAddress, + ) + uiState = stateBuilder.createSuccessState( + uiState = uiState, + txState = it, + dataState = dataState, + txUrl = url, + onSecondaryBtnClick = { + val txHash = it.txAddress + if (txHash.isNotEmpty()) { + swapRouter.openUrl(url) + } + }, + ) analyticsEventHandler.send(SwapEvents.SwapInProgressScreen) swapRouter.openScreen(SwapNavScreen.Success) } From e39af03b9dfef3c4c7862fb58fdf8ec2644dae0c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 18:51:29 +0300 Subject: [PATCH 060/139] Updated on 2026-08-14 --- .../feature/swap/viewmodels/SwapViewModel.kt | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 901ef4daa7..ddf91d7e31 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -437,14 +437,35 @@ internal class SwapViewModel @Inject constructor( } private fun onSearchEntered(searchQuery: String) { - viewModelScope.launch(dispatchers.main) { - runCatching(dispatchers.io) { - swapInteractor.searchTokens(dataState.networkId, searchQuery) + viewModelScope.launch(dispatchers.io) { + val tokenDataState = dataState.tokensDataState ?: return@launch + val group = if (isOrderReversed) { + tokenDataState.fromGroup + } else { + tokenDataState.toGroup } - .onSuccess { - // updateTokensState(it) - } - .onFailure { } + val available = group.available.filter { + it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) + } + val unavailable = group.unavailable.filter { + it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) + } + val filteredTokenDataState = if (isOrderReversed) { + tokenDataState.copy( + fromGroup = tokenDataState.fromGroup.copy( + available = available, + unavailable = unavailable, + ), + ) + } else { + tokenDataState.copy( + toGroup = tokenDataState.toGroup.copy( + available = available, + unavailable = unavailable, + ), + ) + } + updateTokensState(filteredTokenDataState) } } @@ -485,6 +506,7 @@ internal class SwapViewModel @Inject constructor( toProvidersList = findSwapProviders(fromToken, toToken), ) swapRouter.openScreen(SwapNavScreen.Main) + updateTokensState(tokens) } } From 8708876ce52109d747ba0d8714702427a5284b66 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 14:17:17 +0800 Subject: [PATCH 061/139] Updated on 2026-08-14 --- .../transformers/AddWalletTransformer.kt | 23 +++ .../CloseBottomSheetTransformer.kt | 20 +++ .../transformers/DeleteWalletTransformer.kt | 31 ++++ .../InitializeWalletsTransformer.kt | 97 ++++++++++++ .../OpenBottomSheetTransformer.kt | 42 +++++ .../ReinitializeWalletTransformer.kt | 26 ++++ .../transformers/RenameWalletTransformer.kt | 28 ++++ .../transformers/ScrollToWalletTransformer.kt | 30 ++++ .../transformers/SendEventTransformer.kt | 17 +++ .../SetCryptoCurrencyActionsTransformer.kt | 81 ++++++++++ .../SetPrimaryCurrencyTransformer.kt | 47 ++++++ .../SetRefreshStateTransformer.kt | 67 ++++++++ .../SetTokenListErrorTransformer.kt | 38 +++++ .../transformers/SetTokenListTransformer.kt | 69 +++++++++ .../SetTxHistoryCountErrorTransformer.kt | 68 +++++++++ .../SetTxHistoryCountTransformer.kt | 60 ++++++++ .../SetTxHistoryItemsErrorTransformer.kt | 41 +++++ .../SetTxHistoryItemsTransformer.kt | 41 +++++ .../transformers/SetWarningsTransformer.kt | 26 ++++ .../transformers/UnlockWalletTransformer.kt | 53 +++++++ .../UpdateBalanceHidingModeTransformer.kt | 12 ++ .../UpdateWalletCardsCountTransformer.kt | 42 +++++ .../transformers/WalletStateTransformer.kt | 23 +++ .../MultiWalletCardStateConverter.kt | 64 ++++++++ .../MultiWalletCurrencyActionsConverter.kt | 96 ++++++++++++ .../SingleWalletCardStateConverter.kt | 78 ++++++++++ .../SingleWalletMarketPriceConverter.kt | 68 +++++++++ .../converter/TokenItemStateConverter.kt | 122 +++++++++++++++ .../converter/TokenListStateConverter.kt | 93 +++++++++++ .../converter/TxHistoryItemFlowConverter.kt | 144 ++++++++++++++++++ .../converter/TxHistoryItemStateConverter.kt | 109 +++++++++++++ .../state2/utils/UserWalletConverterExt.kt | 16 ++ .../wallet/state2/utils/WalletEventSender.kt | 29 ++++ .../state2/utils/WalletLoadingStateFactory.kt | 85 +++++++++++ .../intents/WalletCardClickIntents.kt | 31 ++++ .../intents/WalletClickIntentsV2.kt | 27 ++++ .../intents/WalletContentClickIntents.kt | 53 +++++++ .../WalletCurrencyActionsClickIntents.kt | 64 ++++++++ .../intents/WalletWarningsClickIntents.kt | 64 ++++++++ 39 files changed, 2125 insertions(+) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/AddWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/CloseBottomSheetTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/DeleteWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/InitializeWalletsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/OpenBottomSheetTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ReinitializeWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/RenameWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ScrollToWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SendEventTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetCryptoCurrencyActionsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetRefreshStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListErrorTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountErrorTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsErrorTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetWarningsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UnlockWalletTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateBalanceHidingModeTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateWalletCardsCountTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCardStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletCardStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletMarketPriceConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenItemStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenListStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemStateConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/UserWalletConverterExt.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletEventSender.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletLoadingStateFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/AddWalletTransformer.kt new file mode 100644 index 0000000000..437c8d5e41 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/AddWalletTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.toImmutableList + +internal class AddWalletTransformer( + private val userWallet: UserWallet, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { + WalletLoadingStateFactory(clickIntents = clickIntents) + } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + wallets = (prevState.wallets + walletLoadingStateFactory.create(userWallet)).toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/CloseBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/CloseBottomSheetTransformer.kt new file mode 100644 index 0000000000..37ddf91e8c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/CloseBottomSheetTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState + +internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(bottomSheetConfig = prevState.bottomSheetConfig?.copy(isShow = false)) + } + is WalletState.MultiCurrency.Locked -> prevState.copy(isBottomSheetShow = false) + is WalletState.SingleCurrency.Content -> { + prevState.copy(bottomSheetConfig = prevState.bottomSheetConfig?.copy(isShow = false)) + } + is WalletState.SingleCurrency.Locked -> prevState.copy(isBottomSheetShow = false) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/DeleteWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/DeleteWalletTransformer.kt new file mode 100644 index 0000000000..34b8081a57 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/DeleteWalletTransformer.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber + +internal class DeleteWalletTransformer( + private val selectedWalletIndex: Int, + private val deletedWalletId: UserWalletId, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + val deletedWalletState = prevState.getDeletedWalletState() + + if (deletedWalletState == null) { + Timber.e("Wallets does not contain deleted wallet") + return prevState + } + + return prevState.copy( + selectedWalletIndex = selectedWalletIndex, + wallets = (prevState.wallets - deletedWalletState).toImmutableList(), + ) + } + + private fun WalletScreenState.getDeletedWalletState(): WalletState? { + return wallets.firstOrNull { it.walletCardState.id == deletedWalletId } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/InitializeWalletsTransformer.kt new file mode 100644 index 0000000000..439a633944 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/InitializeWalletsTransformer.kt @@ -0,0 +1,97 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.state2.utils.createStateByWalletType +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class InitializeWalletsTransformer( + private val selectedWalletIndex: Int, + private val selectedWallet: UserWallet, + private val wallets: List, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + onBackClick = clickIntents::onBackClick, + topBarConfig = createTopBarConfig(userWallet = selectedWallet), + selectedWalletIndex = selectedWalletIndex, + wallets = wallets + .map { userWallet -> + if (userWallet.isLocked) { + createLockedState(userWallet) + } else { + walletLoadingStateFactory.create(userWallet) + } + } + .toImmutableList(), + onWalletChange = clickIntents::onWalletChange, + ) + } + + private fun createTopBarConfig(userWallet: UserWallet): WalletTopBarConfig { + return WalletTopBarConfig( + onDetailsClick = if (userWallet.isLocked) { + clickIntents::onOpenUnlockWalletsBottomSheetClick + } else { + clickIntents::onDetailsClick + }, + ) + } + + private fun createLockedState(userWallet: UserWallet): WalletState { + return userWallet.createStateByWalletType( + multiCurrencyCreator = { + WalletState.MultiCurrency.Locked( + walletCardState = userWallet.toLockedWalletCardState(), + onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, + ) + }, + singleCurrencyCreator = { + WalletState.SingleCurrency.Locked( + walletCardState = userWallet.toLockedWalletCardState(), + buttons = createDisabledButtons(), + onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, + onUnlockClick = clickIntents::onUnlockWalletClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, + onExploreClick = clickIntents::onExploreClick, + ) + }, + ) + } + + private fun UserWallet.toLockedWalletCardState(): WalletCardState { + return WalletCardState.LockedContent( + id = walletId, + title = name, + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = this), + imageResId = WalletImageResolver.resolve(userWallet = this), + onRenameClick = clickIntents::onRenameClick, + onDeleteClick = clickIntents::onDeleteBeforeConfirmationClick, + ) + } + + private fun createDisabledButtons(): PersistentList { + return persistentListOf( + WalletManageButton.Buy(enabled = false, onClick = {}), + WalletManageButton.Send(enabled = false, onClick = {}), + WalletManageButton.Receive(enabled = false, onClick = {}), + WalletManageButton.Sell(enabled = false, onClick = {}), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/OpenBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/OpenBottomSheetTransformer.kt new file mode 100644 index 0000000000..f48797a50f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/OpenBottomSheetTransformer.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState + +internal class OpenBottomSheetTransformer( + userWalletId: UserWalletId, + private val content: TangemBottomSheetConfigContent, + private val onDismissBottomSheet: () -> Unit, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismissBottomSheet, + content = content, + ), + ) + } + is WalletState.MultiCurrency.Locked -> { + prevState.copy(isBottomSheetShow = true, onBottomSheetDismiss = onDismissBottomSheet) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismissBottomSheet, + content = content, + ), + ) + } + is WalletState.SingleCurrency.Locked -> { + prevState.copy(isBottomSheetShow = true, onBottomSheetDismiss = onDismissBottomSheet) + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ReinitializeWalletTransformer.kt new file mode 100644 index 0000000000..ad0ef39b08 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ReinitializeWalletTransformer.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.persistentListOf + +/** +[REDACTED_AUTHOR] + */ +internal class ReinitializeWalletTransformer( + private val userWallet: UserWallet, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + wallets = persistentListOf( + walletLoadingStateFactory.create(userWallet), + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/RenameWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/RenameWalletTransformer.kt new file mode 100644 index 0000000000..80b3692e93 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/RenameWalletTransformer.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import timber.log.Timber + +internal class RenameWalletTransformer( + userWalletId: UserWalletId, + private val newName: String, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.copySealed(title = newName)) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.copySealed(title = newName)) + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to rename wallet in locked state") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ScrollToWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ScrollToWalletTransformer.kt new file mode 100644 index 0000000000..17c76d98e5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/ScrollToWalletTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.common.Provider +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal class ScrollToWalletTransformer( + private val index: Int, + private val currentStateProvider: Provider, + private val stateUpdater: (WalletScreenState) -> Unit, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + event = triggeredEvent( + data = WalletEvent.ChangeWallet(index), + onConsume = { + stateUpdater( + currentStateProvider().copy( + selectedWalletIndex = index, + event = consumedEvent(), + ), + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SendEventTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SendEventTransformer.kt new file mode 100644 index 0000000000..f34236635b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SendEventTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal class SendEventTransformer( + private val event: WalletEvent, + private val onConsume: () -> Unit, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + event = triggeredEvent(data = event, onConsume = onConsume), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetCryptoCurrencyActionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetCryptoCurrencyActionsTransformer.kt new file mode 100644 index 0000000000..605e40f98a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetCryptoCurrencyActionsTransformer.kt @@ -0,0 +1,81 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import timber.log.Timber + +internal class SetCryptoCurrencyActionsTransformer( + private val tokenActionsState: TokenActionsState, + private val userWallet: UserWallet, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + prevState.copy(buttons = tokenActionsState.toManageButtons()) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load primary currency status for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load crypto currency actions for multi-currency wallet") + prevState + } + } + } + + private fun TokenActionsState.toManageButtons(): PersistentList { + return states + .filterIfS2C() + .mapNotNull { action -> + when (action) { + is TokenActionsState.ActionState.Buy -> { + WalletManageButton.Buy( + enabled = action.enabled, + onClick = { clickIntents.onBuyClick(cryptoCurrencyStatus) }, + ) + } + is TokenActionsState.ActionState.Receive -> { + WalletManageButton.Receive( + enabled = action.enabled, + onClick = { clickIntents.onReceiveClick(cryptoCurrencyStatus) }, + ) + } + is TokenActionsState.ActionState.Sell -> { + WalletManageButton.Sell( + enabled = action.enabled, + onClick = { clickIntents.onSellClick(cryptoCurrencyStatus) }, + ) + } + is TokenActionsState.ActionState.Send -> { + WalletManageButton.Send( + enabled = action.enabled, + onClick = { clickIntents.onSendClick(cryptoCurrencyStatus) }, + ) + } + else -> { + null + } + } + } + .toPersistentList() + } + + private fun List.filterIfS2C(): List { + return if (userWallet.scanResponse.cardTypesResolver.isStart2Coin()) { + filterNot { it is TokenActionsState.ActionState.Buy || it is TokenActionsState.ActionState.Sell } + } else { + this + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt new file mode 100644 index 0000000000..83f89adc93 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetPrimaryCurrencyTransformer.kt @@ -0,0 +1,47 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletCardStateConverter +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.SingleWalletMarketPriceConverter +import timber.log.Timber + +internal class SetPrimaryCurrencyTransformer( + private val userWallet: UserWallet, + private val status: CryptoCurrencyStatus.Status, + private val appCurrency: AppCurrency, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + prevState.copy( + walletCardState = prevState.walletCardState.toLoadedState(), + marketPriceBlockState = prevState.marketPriceBlockState.toLoadedState(), + ) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load primary currency status for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load primary currency status for multi-currency wallet") + prevState + } + } + } + + private fun WalletCardState.toLoadedState(): WalletCardState { + return SingleWalletCardStateConverter(status, userWallet, appCurrency).convert(value = this) + } + + private fun MarketPriceBlockState.toLoadedState(): MarketPriceBlockState { + return SingleWalletMarketPriceConverter(status, appCurrency).convert(value = this) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetRefreshStateTransformer.kt new file mode 100644 index 0000000000..043c611ccd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetRefreshStateTransformer.kt @@ -0,0 +1,67 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate + +internal class SetRefreshStateTransformer( + userWalletId: UserWalletId, + private val isRefreshing: Boolean, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy( + pullToRefreshConfig = prevState.pullToRefreshConfig.toUpdatedState(isRefreshing), + tokensListState = prevState.tokensListState.toUpdatedState(), + ) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy( + pullToRefreshConfig = prevState.pullToRefreshConfig.toUpdatedState(isRefreshing), + buttons = prevState.buttons.toUpdatedState(), + ) + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> prevState + } + } + + private fun WalletPullToRefreshConfig.toUpdatedState(isRefreshing: Boolean): WalletPullToRefreshConfig { + return copy(isRefreshing = isRefreshing) + } + + private fun WalletTokensListState.toUpdatedState(): WalletTokensListState { + return if (this is WalletTokensListState.ContentState.Content && organizeTokensButtonConfig != null) { + copy( + organizeTokensButtonConfig = organizeTokensButtonConfig.copy( + isEnabled = !isRefreshing, + ), + ) + } else { + this + } + } + + private fun PersistentList.toUpdatedState(): PersistentList { + val isButtonsEnabled = !isRefreshing + + return mutate { + it.mapNotNull { button -> + when (button) { + is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Receive -> button + is WalletManageButton.Swap -> null + } + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListErrorTransformer.kt new file mode 100644 index 0000000000..4586c30ab6 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListErrorTransformer.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import timber.log.Timber + +internal class SetTokenListErrorTransformer( + userWalletId: UserWalletId, + private val error: TokenListError, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (error) { + is TokenListError.EmptyTokens -> { + when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(tokensListState = WalletTokensListState.Empty) + } + is WalletState.MultiCurrency.Locked, + -> { + Timber.e("Impossible to load tokens list for locked wallet") + prevState + } + is WalletState.SingleCurrency, + -> { + Timber.e("Impossible to load tokens list for single-currency wallet") + prevState + } + } + } + is TokenListError.DataError, + is TokenListError.UnableToSortTokenList, + -> prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListTransformer.kt new file mode 100644 index 0000000000..f982dff23f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTokenListTransformer.kt @@ -0,0 +1,69 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.ManageTokensButtonConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.MultiWalletCardStateConverter +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TokenListStateConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import timber.log.Timber + +internal class SetTokenListTransformer( + private val tokenList: TokenList, + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy( + walletCardState = prevState.walletCardState.toLoadedState(), + tokensListState = prevState.tokensListState.toLoadedState(), + manageTokensButtonConfig = createManageTokensButtonConfig(), + ) + } + is WalletState.MultiCurrency.Locked, + -> { + Timber.e("Impossible to load tokens list for locked wallet") + prevState + } + is WalletState.SingleCurrency, + -> { + Timber.e("Impossible to load tokens list for single-currency wallet") + prevState + } + } + } + + private fun WalletCardState.toLoadedState(): WalletCardState { + return MultiWalletCardStateConverter( + fiatBalance = tokenList.totalFiatBalance, + selectedWallet = userWallet, + appCurrency = appCurrency, + ).convert(value = this) + } + + private fun WalletTokensListState.toLoadedState(): WalletTokensListState { + return TokenListStateConverter( + tokenList = tokenList, + selectedWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + ).convert(value = this) + } + + private fun createManageTokensButtonConfig(): ManageTokensButtonConfig? { + return if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + null + } else { + ManageTokensButtonConfig(clickIntents::onManageTokensClick) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountErrorTransformer.kt new file mode 100644 index 0000000000..26f55b70a4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountErrorTransformer.kt @@ -0,0 +1,68 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TxHistoryItemStateConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber + +internal class SetTxHistoryCountErrorTransformer( + private val userWallet: UserWallet, + private val error: TxHistoryStateError, + private val pendingTransactions: Set, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + private val txHistoryItemConverter by lazy { + val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + TxHistoryItemStateConverter( + symbol = blockchain.currency, + decimals = blockchain.decimals(), + clickIntents = clickIntents, + ) + } + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> prevState.toErrorState() + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } + + private fun WalletState.SingleCurrency.Content.toErrorState(): WalletState { + return copy( + txHistoryState = when (error) { + is TxHistoryStateError.EmptyTxHistories -> { + TxHistoryState.Empty(onExploreClick = clickIntents::onExploreClick) + } + is TxHistoryStateError.DataError -> { + TxHistoryState.Error( + onReloadClick = clickIntents::onReloadClick, + onExploreClick = clickIntents::onExploreClick, + ) + } + is TxHistoryStateError.TxHistoryNotImplemented -> { + TxHistoryState.NotSupported( + pendingTransactions = txHistoryItemConverter.convertList(pendingTransactions) + .toImmutableList(), + onExploreClick = clickIntents::onExploreClick, + ) + } + }, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountTransformer.kt new file mode 100644 index 0000000000..082d5aa124 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryCountTransformer.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import androidx.paging.PagingData +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber + +internal class SetTxHistoryCountTransformer( + userWalletId: UserWalletId, + private val transactionsCount: Int, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> prevState.toLoadingState() + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency, + -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } + + private fun WalletState.SingleCurrency.Content.toLoadingState(): WalletState { + return if (txHistoryState is TxHistoryState.Content) { + (txHistoryState as? TxHistoryState.Content)?.contentItems?.update { + Timber.d("Load transactions history: $transactionsCount") + PagingData.from(data = createLoadingItems()) + } + this + } else { + val txHistoryContent = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = PagingData.from(data = createLoadingItems()), + ), + ) + copy(txHistoryState = txHistoryContent) + } + } + + private fun createLoadingItems(): List { + return buildList { + add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + (1..transactionsCount).forEach { + add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString()))) + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsErrorTransformer.kt new file mode 100644 index 0000000000..37e26fa534 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsErrorTransformer.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import timber.log.Timber + +internal class SetTxHistoryItemsErrorTransformer( + userWalletId: UserWalletId, + private val error: TxHistoryListError, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + prevState.copy( + txHistoryState = when (error) { + is TxHistoryListError.DataError -> { + TxHistoryState.Error( + onReloadClick = clickIntents::onReloadClick, + onExploreClick = clickIntents::onExploreClick, + ) + } + }, + ) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsTransformer.kt new file mode 100644 index 0000000000..1bd03fe9b1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetTxHistoryItemsTransformer.kt @@ -0,0 +1,41 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import androidx.paging.PagingData +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.TxHistoryItemFlowConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.flow.Flow +import timber.log.Timber + +internal class SetTxHistoryItemsTransformer( + private val userWallet: UserWallet, + private val flow: Flow>, + private val clickIntents: WalletClickIntentsV2, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + val converter = TxHistoryItemFlowConverter( + userWallet = userWallet, + currentState = prevState, + clickIntents = clickIntents, + ) + prevState.copy( + txHistoryState = converter.convert(value = flow), + ) + } + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to load transactions history for locked wallet") + prevState + } + is WalletState.MultiCurrency -> { + Timber.e("Impossible to load transactions history for multi-currency wallet") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetWarningsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetWarningsTransformer.kt new file mode 100644 index 0000000000..064c72d1e7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/SetWarningsTransformer.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import kotlinx.collections.immutable.ImmutableList +import timber.log.Timber + +internal class SetWarningsTransformer( + userWalletId: UserWalletId, + private val warnings: ImmutableList, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> prevState.copy(warnings = warnings) + is WalletState.SingleCurrency.Content -> prevState.copy(warnings = warnings) + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to update notifications for locked wallet") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UnlockWalletTransformer.kt new file mode 100644 index 0000000000..bd5c0bd61b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UnlockWalletTransformer.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletLoadingStateFactory +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber + +internal class UnlockWalletTransformer( + private val unlockedWallets: List, + private val clickIntents: WalletClickIntentsV2, +) : WalletScreenStateTransformer { + + private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory(clickIntents = clickIntents) } + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + topBarConfig = prevState.topBarConfig.toUnlockedState(), + wallets = prevState.wallets + .map { state -> + val unlockedWallet = getUnlockedWallet(state.walletCardState.id) + if (unlockedWallet == null) state else createLoadingState(state, unlockedWallet) + } + .toImmutableList(), + ) + } + + private fun WalletTopBarConfig.toUnlockedState(): WalletTopBarConfig { + return copy(onDetailsClick = clickIntents::onDetailsClick) + } + + private fun getUnlockedWallet(walletId: UserWalletId): UserWallet? { + return unlockedWallets.firstOrNull { it.walletId == walletId } + } + + private fun createLoadingState(prevState: WalletState, unlockedWallet: UserWallet): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> walletLoadingStateFactory.create(userWallet = unlockedWallet) + is WalletState.MultiCurrency.Content, + is WalletState.SingleCurrency.Content, + -> { + Timber.e("Impossible to unlock wallet with content state") + prevState + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateBalanceHidingModeTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateBalanceHidingModeTransformer.kt new file mode 100644 index 0000000000..6dd7da1e64 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateBalanceHidingModeTransformer.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState + +internal class UpdateBalanceHidingModeTransformer( + private val isHidingMode: Boolean, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy(isHidingMode = isHidingMode) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateWalletCardsCountTransformer.kt new file mode 100644 index 0000000000..fe89479e52 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/UpdateWalletCardsCountTransformer.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import timber.log.Timber + +internal class UpdateWalletCardsCountTransformer( + private val userWallet: UserWallet, +) : WalletStateTransformer(userWallet.walletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState()) + } + is WalletState.SingleCurrency.Content -> { + prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState()) + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> { + Timber.e("Impossible to update wallet cards count for locked wallet") + prevState + } + } + } + + private fun WalletCardState.toUpdatedState(): WalletCardState { + return when (this) { + is WalletCardState.Content -> copy( + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet), + imageResId = WalletImageResolver.resolve(userWallet = userWallet), + cardCount = userWallet.getCardsCount(), + ) + else -> this + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletStateTransformer.kt new file mode 100644 index 0000000000..38af9d7f93 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/WalletStateTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import kotlinx.collections.immutable.toImmutableList + +internal abstract class WalletStateTransformer( + protected val userWalletId: UserWalletId, +) : WalletScreenStateTransformer { + + abstract fun transform(prevState: WalletState): WalletState + + override fun transform(prevState: WalletScreenState): WalletScreenState { + return prevState.copy( + wallets = prevState.wallets + .map { state -> + if (state.walletCardState.id == userWalletId) transform(state) else state + } + .toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCardStateConverter.kt new file mode 100644 index 0000000000..8c3b84bf05 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCardStateConverter.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.utils.converter.Converter + +internal class MultiWalletCardStateConverter( + private val fiatBalance: TokenList.FiatBalance, + private val selectedWallet: UserWallet, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: WalletCardState): WalletCardState { + return when (fiatBalance) { + is TokenList.FiatBalance.Loading -> value.toLoadingState() + is TokenList.FiatBalance.Failed -> value.toErrorState() + is TokenList.FiatBalance.Loaded -> value.toWalletCardState(fiatBalance) + } + } + + private fun WalletCardState.toLoadingState(): WalletCardState { + return WalletCardState.Loading( + id = id, + title = title, + additionalInfo = additionalInfo, + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun WalletCardState.toErrorState(): WalletCardState { + return WalletCardState.Error( + id = id, + title = title, + additionalInfo = additionalInfo, + imageResId = imageResId, + onDeleteClick = onDeleteClick, + onRenameClick = onRenameClick, + ) + } + + private fun WalletCardState.toWalletCardState(fiatBalance: TokenList.FiatBalance.Loaded): WalletCardState { + return WalletCardState.Content( + id = id, + title = title, + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet), + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + balance = BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatBalance.amount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + cardCount = selectedWallet.getCardsCount(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt new file mode 100644 index 0000000000..270fa05c9c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -0,0 +1,96 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletCurrencyActionsClickIntentsImplementor +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class MultiWalletCurrencyActionsConverter( + private val userWallet: UserWallet, + private val clickIntents: WalletCurrencyActionsClickIntentsImplementor, +) : Converter> { + + override fun convert(value: TokenActionsState): ImmutableList { + return value.states + .filterIfSingleWithToken() + .mapNotNull { + mapTokenActionState(actionsState = it, cryptoCurrencyStatus = value.cryptoCurrencyStatus) + } + .toImmutableList() + } + + private fun List.filterIfSingleWithToken(): List { + return if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + filter { it !is TokenActionsState.ActionState.HideToken } + } else { + this + } + } + + private fun mapTokenActionState( + actionsState: TokenActionsState.ActionState, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): TokenActionButtonConfig? { + if (actionsState is TokenActionsState.ActionState.Send && cryptoCurrencyStatus.value.amount.isNullOrZero()) { + return null + } + + val title: TextReference + val icon: Int + val action: () -> Unit + when (actionsState) { + is TokenActionsState.ActionState.Buy -> { + title = resourceReference(R.string.common_buy) + icon = R.drawable.ic_plus_24 + action = { clickIntents.onBuyClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Receive -> { + title = resourceReference(R.string.common_receive) + icon = R.drawable.ic_arrow_down_24 + action = { clickIntents.onReceiveClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Sell -> { + title = resourceReference(R.string.common_sell) + icon = R.drawable.ic_currency_24 + action = { clickIntents.onSellClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Send -> { + title = resourceReference(R.string.common_send) + icon = R.drawable.ic_arrow_up_24 + action = { clickIntents.onSendClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Swap -> { + title = resourceReference(R.string.common_swap) + icon = R.drawable.ic_exchange_horizontal_24 + action = { clickIntents.onSwapClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.CopyAddress -> { + title = resourceReference(R.string.common_copy_address) + icon = R.drawable.ic_copy_24 + action = { clickIntents.onCopyAddressClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.HideToken -> { + title = resourceReference(R.string.token_details_hide_token) + icon = R.drawable.ic_hide_24 + action = { clickIntents.onHideTokensClick(cryptoCurrencyStatus) } + } + } + + return TokenActionButtonConfig( + text = title, + iconResId = icon, + onClick = action, + isWarning = actionsState is TokenActionsState.ActionState.HideToken, + enabled = actionsState.enabled, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletCardStateConverter.kt new file mode 100644 index 0000000000..e54cf09dfa --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletCardStateConverter.kt @@ -0,0 +1,78 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.utils.converter.Converter + +internal class SingleWalletCardStateConverter( + private val status: CryptoCurrencyStatus.Status, + private val selectedWallet: UserWallet, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: WalletCardState): WalletCardState { + return when (status) { + is CryptoCurrencyStatus.Loading -> value.toLoadingState() + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + -> value.toErrorState() + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.NoAmount, + -> value.toContentState(status) + } + } + + private fun WalletCardState.toLoadingState(): WalletCardState { + return WalletCardState.Loading( + id = id, + title = title, + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun WalletCardState.toErrorState(): WalletCardState { + return WalletCardState.Error( + id = id, + title = title, + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun WalletCardState.toContentState(status: CryptoCurrencyStatus.Status): WalletCardState { + return WalletCardState.Content( + id = id, + title = title, + additionalInfo = WalletAdditionalInfoFactory.resolve( + wallet = selectedWallet, + currencyAmount = status.amount, + ), + imageResId = imageResId, + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + balance = formatFiatAmount(status = status, appCurrency = appCurrency), + cardCount = selectedWallet.getCardsCount(), + ) + } + + private fun formatFiatAmount(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletMarketPriceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletMarketPriceConverter.kt new file mode 100644 index 0000000000..1f1237f976 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -0,0 +1,68 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class SingleWalletMarketPriceConverter( + private val status: CryptoCurrencyStatus.Status, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: MarketPriceBlockState): MarketPriceBlockState { + return when (status) { + CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(value.currencySymbol) + is CryptoCurrencyStatus.NoAccount -> value.toNoAccountState() + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.NoAmount, + -> value.toContentState() + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.Unreachable, + -> MarketPriceBlockState.Error(value.currencySymbol) + } + } + + private fun MarketPriceBlockState.toNoAccountState(): MarketPriceBlockState { + return if (status.fiatRate == null) MarketPriceBlockState.Error(currencySymbol) else toContentState() + } + + private fun MarketPriceBlockState.toContentState(): MarketPriceBlockState { + return MarketPriceBlockState.Content( + currencySymbol = currencySymbol, + price = formatPrice(status = status, appCurrency = appCurrency), + priceChangeConfig = PriceChangeState.Content( + valueInPercent = formatPriceChange(status = status), + type = getPriceChangeType(status = status), + ), + ) + } + + private fun formatPrice(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatRate, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { + val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + + return BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true) + } + + private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType { + val priceChange = status.priceChange ?: return PriceChangeType.DOWN + + return if (priceChange > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenItemStateConverter.kt new file mode 100644 index 0000000000..eef6aea5c9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenItemStateConverter.kt @@ -0,0 +1,122 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class TokenItemStateConverter( + private val appCurrencyProvider: Provider, + private val clickIntents: WalletClickIntentsV2, +) : Converter { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + override fun convert(value: CryptoCurrencyStatus): TokenItemState { + return when (value.value) { + is CryptoCurrencyStatus.Loading -> value.mapToLoadingState() + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> value.mapToTokenItemState() + is CryptoCurrencyStatus.MissedDerivation -> value.mapToNoAddressTokenItemState() + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> value.mapToUnreachableTokenItemState() + } + } + + private fun CryptoCurrencyStatus.mapToLoadingState(): TokenItemState.Loading { + return TokenItemState.Loading( + id = currency.id.value, + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content(text = currency.name), + ) + } + + private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content { + return TokenItemState.Content( + id = currency.id.value, + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content( + text = currency.name, + hasPending = value.hasCurrentNetworkTransactions, + ), + fiatAmountState = TokenItemState.FiatAmountState.Content( + text = getFormattedFiatAmount(), + ), + cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = getFormattedAmount()), + cryptoPriceState = getCryptoPriceState(), + onItemClick = { clickIntents.onTokenItemClick(currency) }, + onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, + ) + } + + private fun CryptoCurrencyStatus.getFormattedAmount(): String { + val amount = value.amount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + + return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) + } + + private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { + val fiatAmount = value.fiatAmount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + val appCurrency = appCurrencyProvider() + + return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) + } + + private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( + id = currency.id.value, + iconState = iconStateConverter.convert(value = this), + titleState = TokenItemState.TitleState.Content(text = currency.name), + onItemClick = { clickIntents.onTokenItemClick(currency) }, + onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, + ) + + private fun CryptoCurrencyStatus.mapToNoAddressTokenItemState() = TokenItemState.NoAddress( + id = currency.id.value, + iconState = iconStateConverter.convert(this), + titleState = TokenItemState.TitleState.Content(text = currency.name), + onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, + ) + + private fun CryptoCurrencyStatus.getCryptoPriceState(): TokenItemState.CryptoPriceState { + val fiatRate = value.fiatRate + val priceChange = value.priceChange + + return if (fiatRate != null && priceChange != null) { + TokenItemState.CryptoPriceState.Content( + price = fiatRate.getFormattedCryptoPrice(), + priceChangePercent = BigDecimalFormatter.formatPercent( + percent = priceChange, + useAbsoluteValue = true, + maxFractionDigits = 1, + minFractionDigits = 1, + ), + type = priceChange.getPriceChangeType(), + ) + } else { + TokenItemState.CryptoPriceState.Unknown + } + } + + private fun BigDecimal.getFormattedCryptoPrice(): String { + val appCurrency = appCurrencyProvider() + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = this, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun BigDecimal.getPriceChangeType(): PriceChangeType { + return if (this > BigDecimal.ZERO) PriceChangeType.UP else PriceChangeType.DOWN + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenListStateConverter.kt new file mode 100644 index 0000000000..b9b2ab5cf3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TokenListStateConverter.kt @@ -0,0 +1,93 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.common.Provider +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.TokensListItemState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig + +internal class TokenListStateConverter( + private val tokenList: TokenList, + private val selectedWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, +) : Converter { + + private val tokenStatusConverter = TokenItemStateConverter( + appCurrencyProvider = Provider { appCurrency }, + clickIntents = clickIntents, + ) + + override fun convert(value: WalletTokensListState): WalletTokensListState { + return when (tokenList) { + is TokenList.Empty -> WalletTokensListState.Empty + is TokenList.GroupedByNetwork -> WalletTokensListState.ContentState.Content( + items = tokenList.toGroupedItems(), + organizeTokensButtonConfig = getOrganizeTokensButtonState( + currenciesSize = tokenList.groups.flatMap(NetworkGroup::currencies).size, + ), + ) + is TokenList.Ungrouped -> WalletTokensListState.ContentState.Content( + items = tokenList.toUngroupedItems(), + organizeTokensButtonConfig = getOrganizeTokensButtonState(currenciesSize = tokenList.currencies.size), + ) + } + } + + private fun TokenList.GroupedByNetwork.toGroupedItems(): PersistentList { + return groups.fold(initial = persistentListOf()) { acc, group -> + acc.mutate { it.addGroup(group) } + } + } + + private fun TokenList.Ungrouped.toUngroupedItems(): PersistentList { + return currencies.fold(initial = persistentListOf()) { acc, token -> + acc.mutate { it.addToken(token) } + } + } + + private fun MutableList.addGroup(group: NetworkGroup): List { + val groupTitle = TokensListItemState.NetworkGroupTitle( + id = group.network.hashCode(), + name = stringReference(group.network.name), + ) + + add(groupTitle) + group.currencies.forEach { token -> addToken(token) } + + return this + } + + private fun MutableList.addToken(token: CryptoCurrencyStatus): List { + val tokenItemState = tokenStatusConverter.convert(token) + add(TokensListItemState.Token(tokenItemState)) + + return this + } + + private fun getOrganizeTokensButtonState(currenciesSize: Int): WalletOrganizeTokensButtonConfig? { + return if (currenciesSize > 1 && !isSingleCurrencyWalletWithToken()) { + WalletOrganizeTokensButtonConfig( + isEnabled = tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading, + onClick = clickIntents::onOrganizeTokensClick, + ) + } else { + null + } + } + + private fun isSingleCurrencyWalletWithToken(): Boolean { + return selectedWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt new file mode 100644 index 0000000000..d8209a170d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt @@ -0,0 +1,144 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import android.text.format.DateUtils +import androidx.paging.* +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import java.util.UUID + +private val scope = CoroutineScope(Dispatchers.IO) + +internal class TxHistoryItemFlowConverter( + private val userWallet: UserWallet, + private val currentState: WalletState.SingleCurrency.Content, + private val clickIntents: WalletClickIntentsV2, +) : Converter>, TxHistoryState?> { + + private val txHistoryItemConverter by lazy { + val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + TxHistoryItemStateConverter( + symbol = blockchain.currency, + decimals = blockchain.decimals(), + clickIntents = clickIntents, + ) + } + + override fun convert(value: Flow>): TxHistoryState { + val txHistoryContent = currentState.txHistoryState as? TxHistoryState.Content + ?: TxHistoryState.Content(contentItems = MutableStateFlow(PagingData.empty())) + + // FIXME: TxHistoryRepository should send loading transactions + // [REDACTED_JIRA] + value + .onEach { txHistoryStatePagingData -> + txHistoryContent.contentItems.update { + txHistoryStatePagingData + .map { item -> + // [createTransactionState] returns timestamp without formatting + TxHistoryItemState.Transaction(state = createTransactionState(item)) + } + .insertHeaderItem( + terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE, + item = TxHistoryItemState.Title(clickIntents::onExploreClick), + ) + .insertGroupTitle() // method uses the raw timestamp + .formatTransactionsTimestamp() // method formats the timestamp + } + } + .cachedIn(scope) + .launchIn(scope) + + return txHistoryContent + } + + private fun createTransactionState(item: TxHistoryItem): TransactionState { + return txHistoryItemConverter.convert(value = item) + } + + private fun PagingData.insertGroupTitle(): PagingData { + return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> + // Use raw timestamp to get date + + // If [afterDate] is the first transaction in the flow, add the group title + val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + if (before is TxHistoryItemState.Title) { + return@insertSeparators TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString()) + } + + /* + * If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in + * the new group + */ + val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + return@insertSeparators if (beforeDate != afterDate) { + TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString()) + } else { + null + } + } + } + + /** + * Map the [PagingData] to format the [TxHistoryItemState] timestamp + */ + private fun PagingData.formatTransactionsTimestamp(): PagingData { + return map { txHistoryItemState -> + if (txHistoryItemState is TxHistoryItemState.Transaction && + txHistoryItemState.state is TransactionState.Content + ) { + val txContent = txHistoryItemState.state as TransactionState.Content + txHistoryItemState.copy( + state = txContent.copy(timestamp = txContent.timestamp.toTimeFormat()), + ) + } else { + txHistoryItemState + } + } + } + + private fun TxHistoryItemState?.getTimestamp(): Long? { + return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) { + val txContent = this.state as TransactionState.Content + requireNotNull(txContent.timestamp.toLongOrNull()) { "Timestamp must be Long type" } + } else { + null + } + } + + /** + * If [this] timestamp is today or yesterday, returns relative date, + * otherwise returns formatting date. + */ + private fun Long.toDateFormat(): String { + val localDate = DateTime(this, DateTimeZone.getDefault()) + return if (localDate.isToday() || localDate.isYesterday()) { + DateUtils.getRelativeTimeSpanString( + this, + DateTime.now().millis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + } else { + DateTimeFormatters.formatDate(date = localDate) + } + } + + private fun String.toTimeFormat(): String { + return DateTimeFormatters.formatTime(time = DateTime(this.toLong(), DateTimeZone.getDefault())) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemStateConverter.kt new file mode 100644 index 0000000000..e9c78803f3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemStateConverter.kt @@ -0,0 +1,109 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter + +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.converter.Converter +import com.tangem.utils.toBriefAddressFormat +import com.tangem.utils.toFormattedCurrencyString + +internal class TxHistoryItemStateConverter( + private val symbol: String, + private val decimals: Int, + private val clickIntents: WalletClickIntentsV2, +) : Converter { + + override fun convert(value: TxHistoryItem): TransactionState { + return createTransactionStateItem(item = value) + } + + @Suppress("LongMethod") + private fun createTransactionStateItem(item: TxHistoryItem): TransactionState { + return TransactionState.Content( + txHash = item.txHash, + amount = item.getAmount(), + timestamp = item.getRawTimestamp(), + status = item.status.tiUiStatus(), + direction = item.extractDirection(), + iconRes = item.extractIcon(), + title = item.extractTitle(), + subtitle = item.extractSubtitle(), + onClick = { clickIntents.onTransactionClick(item.txHash) }, + ) + } + + private fun TxHistoryItem.extractIcon(): Int = if (status == TxHistoryItem.TransactionStatus.Failed) { + R.drawable.ic_close_24 + } else { + when (type) { + is TxHistoryItem.TransactionType.Approve -> R.drawable.ic_doc_24 + is TxHistoryItem.TransactionType.Operation, + is TxHistoryItem.TransactionType.Swap, + is TxHistoryItem.TransactionType.Transfer, + is TxHistoryItem.TransactionType.UnknownOperation, + -> if (isOutgoing) R.drawable.ic_arrow_up_24 else R.drawable.ic_arrow_down_24 + } + } + + private fun TxHistoryItem.extractTitle(): TextReference = when (val type = type) { + is TxHistoryItem.TransactionType.Approve -> resourceReference(R.string.common_approval) + is TxHistoryItem.TransactionType.Operation -> stringReference(type.name) + is TxHistoryItem.TransactionType.Swap -> resourceReference(R.string.common_swap) + is TxHistoryItem.TransactionType.Transfer -> resourceReference(R.string.common_transfer) + is TxHistoryItem.TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) + } + + private fun TxHistoryItem.extractSubtitle(): TextReference = + when (val interactionAddress = interactionAddressType) { + is TxHistoryItem.InteractionAddressType.Contract -> resourceReference( + id = R.string.transaction_history_contract_address, + formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), + ) + is TxHistoryItem.InteractionAddressType.Multiple -> resourceReference( + id = if (isOutgoing) { + R.string.transaction_history_transaction_to_address + } else { + R.string.transaction_history_transaction_from_address + }, + formatArgs = wrappedList(resourceReference(R.string.transaction_history_multiple_addresses)), + ) + is TxHistoryItem.InteractionAddressType.User -> resourceReference( + id = if (isOutgoing) { + R.string.transaction_history_transaction_to_address + } else { + R.string.transaction_history_transaction_from_address + }, + formatArgs = wrappedList(interactionAddress.address.toBriefAddressFormat()), + ) + } + + private fun TxHistoryItem.extractDirection() = + if (isOutgoing) TransactionState.Content.Direction.OUTGOING else TransactionState.Content.Direction.INCOMING + + /** + * Get timestamp without formatting. + * It's life hack that help us to add transaction's group title to flow. + * + * @see [convert] + */ + private fun TxHistoryItem.getRawTimestamp() = this.timestampInMillis.toString() + + private fun TxHistoryItem.TransactionStatus.tiUiStatus() = when (this) { + TxHistoryItem.TransactionStatus.Confirmed -> TransactionState.Content.Status.Confirmed + TxHistoryItem.TransactionStatus.Failed -> TransactionState.Content.Status.Failed + TxHistoryItem.TransactionStatus.Unconfirmed -> TransactionState.Content.Status.Unconfirmed + } + + private fun TxHistoryItem.getAmount(): String { + val prefix = when (status) { + TxHistoryItem.TransactionStatus.Failed -> "" + else -> if (isOutgoing) "-" else "+" + } + return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/UserWalletConverterExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/UserWalletConverterExt.kt new file mode 100644 index 0000000000..75d10c7b80 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/UserWalletConverterExt.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.utils + +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState + +internal inline fun UserWallet.createStateByWalletType( + multiCurrencyCreator: () -> WalletState.MultiCurrency, + singleCurrencyCreator: () -> WalletState.SingleCurrency, +): WalletState { + return if (isWalletWithTokens()) multiCurrencyCreator() else singleCurrencyCreator() +} + +private fun UserWallet.isWalletWithTokens(): Boolean { + return isMultiCurrency || scanResponse.cardTypesResolver.isSingleWalletWithToken() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletEventSender.kt new file mode 100644 index 0000000000..6cc3953821 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletEventSender.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.utils + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SendEventTransformer +import javax.inject.Inject + +/** + * Component for sending events [WalletEvent] on WalletScreen + * + * @property stateHolder state holder for changing state + * +[REDACTED_AUTHOR] + */ +internal class WalletEventSender @Inject constructor( + private val stateHolder: WalletStateHolderV2, +) { + + fun send(event: WalletEvent) { + stateHolder.update(transformer = SendEventTransformer(event = event, onConsume = ::onConsume)) + } + + private fun onConsume() { + stateHolder.update { + it.copy(event = consumedEvent()) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletLoadingStateFactory.kt new file mode 100644 index 0000000000..055d23befa --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/utils/WalletLoadingStateFactory.kt @@ -0,0 +1,85 @@ +package com.tangem.feature.wallet.presentation.wallet.state2.utils + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig +import com.tangem.feature.wallet.presentation.wallet.state2.ManageTokensButtonConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * Factory for creating loading state [WalletState] + * + * @property clickIntents click intents + */ +internal class WalletLoadingStateFactory(private val clickIntents: WalletClickIntentsV2) { + + fun create(userWallet: UserWallet): WalletState { + return userWallet.createStateByWalletType( + multiCurrencyCreator = { createLoadingMultiCurrencyContent(userWallet) }, + singleCurrencyCreator = { createLoadingSingleCurrencyContent(userWallet) }, + ) + } + + private fun createLoadingMultiCurrencyContent(userWallet: UserWallet): WalletState.MultiCurrency.Content { + return WalletState.MultiCurrency.Content( + pullToRefreshConfig = createPullToRefreshConfig(), + walletCardState = userWallet.toLoadingWalletCardState(), + warnings = persistentListOf(), + bottomSheetConfig = null, + tokensListState = WalletTokensListState.ContentState.Loading, + manageTokensButtonConfig = ManageTokensButtonConfig(clickIntents::onManageTokensClick), + ) + } + + private fun createLoadingSingleCurrencyContent(userWallet: UserWallet): WalletState.SingleCurrency.Content { + val currencySymbol = userWallet.scanResponse.cardTypesResolver.getBlockchain().currency + return WalletState.SingleCurrency.Content( + pullToRefreshConfig = createPullToRefreshConfig(), + walletCardState = userWallet.toLoadingWalletCardState(), + warnings = persistentListOf(), + bottomSheetConfig = null, + buttons = createDisabledButtons(), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = currencySymbol), + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), + ) + } + + private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { + return WalletPullToRefreshConfig(onRefresh = clickIntents::onRefreshSwipe, isRefreshing = false) + } + + private fun UserWallet.toLoadingWalletCardState(): WalletCardState { + return WalletCardState.Loading( + id = walletId, + title = name, + additionalInfo = if (isMultiCurrency) WalletAdditionalInfoFactory.resolve(wallet = this) else null, + imageResId = WalletImageResolver.resolve(userWallet = this), + onRenameClick = clickIntents::onRenameClick, + onDeleteClick = clickIntents::onDeleteBeforeConfirmationClick, + ) + } + + private fun createDisabledButtons(): PersistentList { + return persistentListOf( + WalletManageButton.Buy(enabled = false, onClick = {}), + WalletManageButton.Send(enabled = false, onClick = {}), + WalletManageButton.Receive(enabled = false, onClick = {}), + WalletManageButton.Sell(enabled = false, onClick = {}), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt new file mode 100644 index 0000000000..29df476122 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.domain.wallets.models.UserWalletId +import javax.inject.Inject + +/** +[REDACTED_AUTHOR] + */ +internal interface WalletCardClickIntents { + + fun onRenameClick(userWalletId: UserWalletId, name: String) + + fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) + + fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) +} + +internal class WalletCardClickIntentsImplementor @Inject constructor() : WalletCardClickIntents { + + override fun onRenameClick(userWalletId: UserWalletId, name: String) { + // TODO + } + + override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) { + // TODO + } + + override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { + // TODO + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt new file mode 100644 index 0000000000..4903e65c4d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import javax.inject.Inject + +internal class WalletClickIntentsV2 @Inject constructor( + private val walletCardClickIntentsImplementor: WalletCardClickIntentsImplementor, + private val warningsClickIntentsImplementer: WalletWarningsClickIntentsImplementer, + private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, + private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, +) : WalletCardClickIntents by walletCardClickIntentsImplementor, + WalletWarningsClickIntents by warningsClickIntentsImplementer, + WalletCurrencyActionsClickIntents by currencyActionsClickIntentsImplementor, + WalletContentClickIntents by contentClickIntentsImplementor { + + @Suppress("UnusedPrivateMember") + fun onWalletChange(index: Int) { + // TODO + } + + fun onRefreshSwipe() { + // TODO + } + + fun onReloadClick() { + // TODO + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt new file mode 100644 index 0000000000..02fc91d677 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import javax.inject.Inject + +internal interface WalletContentClickIntents { + + fun onBackClick() + + fun onDetailsClick() + + fun onManageTokensClick() + + fun onOrganizeTokensClick() + + fun onTokenItemClick(currency: CryptoCurrency) + + fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onTransactionClick(txHash: String) +} + +internal class WalletContentClickIntentsImplementor @Inject constructor() : WalletContentClickIntents { + + override fun onBackClick() { + // TODO + } + + override fun onDetailsClick() { + // TODO + } + + override fun onManageTokensClick() { + // TODO + } + + override fun onOrganizeTokensClick() { + // TODO + } + + override fun onTokenItemClick(currency: CryptoCurrency) { + // TODO + } + + override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onTransactionClick(txHash: String) { + // TODO + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt new file mode 100644 index 0000000000..dd10ad4337 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import javax.inject.Inject + +interface WalletCurrencyActionsClickIntents { + + fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onHideTokensClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onExploreClick() +} + +internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor() : WalletCurrencyActionsClickIntents { + + override fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onHideTokensClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO + } + + override fun onExploreClick() { + // TODO + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt new file mode 100644 index 0000000000..99dfe53ffb --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.domain.tokens.model.CryptoCurrency +import javax.inject.Inject + +internal interface WalletWarningsClickIntents { + + fun onAddBackupCardClick() + + fun onCloseAlreadySignedHashesWarningClick() + + fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) + + fun onOpenUnlockWalletsBottomSheetClick() + + fun onUnlockWalletClick() + + fun onScanToUnlockWalletClick() + + fun onLikeAppClick() + + fun onDislikeAppClick() + + fun onCloseRateAppWarningClick() +} + +internal class WalletWarningsClickIntentsImplementer @Inject constructor() : WalletWarningsClickIntents { + + override fun onAddBackupCardClick() { + // TODO + } + + override fun onCloseAlreadySignedHashesWarningClick() { + // TODO + } + + override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) { + // TODO + } + + override fun onOpenUnlockWalletsBottomSheetClick() { + // TODO + } + + override fun onUnlockWalletClick() { + // TODO + } + + override fun onScanToUnlockWalletClick() { + // TODO + } + + override fun onLikeAppClick() { + // TODO + } + + override fun onDislikeAppClick() { + // TODO + } + + override fun onCloseRateAppWarningClick() { + // TODO + } +} \ No newline at end of file From aabfc1bba15cb78d473b808be2ef31d4f42dc7fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 20:04:12 +0800 Subject: [PATCH 062/139] Updated on 2026-08-14 --- .../converter/TxHistoryItemFlowConverter.kt | 32 ++----------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt index d8209a170d..6aeb577d67 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/TxHistoryItemFlowConverter.kt @@ -1,24 +1,20 @@ package com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter -import android.text.format.DateUtils import androidx.paging.* import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState -import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state2.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.isToday -import com.tangem.utils.extensions.isYesterday import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* -import org.joda.time.DateTime -import org.joda.time.DateTimeZone import java.util.UUID private val scope = CoroutineScope(Dispatchers.IO) @@ -103,7 +99,7 @@ internal class TxHistoryItemFlowConverter( ) { val txContent = txHistoryItemState.state as TransactionState.Content txHistoryItemState.copy( - state = txContent.copy(timestamp = txContent.timestamp.toTimeFormat()), + state = txContent.copy(timestamp = txContent.timestamp.toLong().toTimeFormat()), ) } else { txHistoryItemState @@ -119,26 +115,4 @@ internal class TxHistoryItemFlowConverter( null } } - - /** - * If [this] timestamp is today or yesterday, returns relative date, - * otherwise returns formatting date. - */ - private fun Long.toDateFormat(): String { - val localDate = DateTime(this, DateTimeZone.getDefault()) - return if (localDate.isToday() || localDate.isYesterday()) { - DateUtils.getRelativeTimeSpanString( - this, - DateTime.now().millis, - DateUtils.DAY_IN_MILLIS, - DateUtils.FORMAT_ABBREV_RELATIVE, - ).toString() - } else { - DateTimeFormatters.formatDate(date = localDate) - } - } - - private fun String.toTimeFormat(): String { - return DateTimeFormatters.formatTime(time = DateTime(this.toLong(), DateTimeZone.getDefault())) - } } \ No newline at end of file From 422afbc17f24d62fbe51cfdf1a4868ed149cd4ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 14:58:11 +0800 Subject: [PATCH 063/139] Updated on 2026-08-14 --- .../presentation/wallet/domain/UseCaseExt.kt | 60 +++ .../intents/BaseWalletClickIntents.kt | 26 ++ .../intents/WalletCardClickIntents.kt | 54 ++- .../intents/WalletClickIntentsV2.kt | 133 ++++++- .../intents/WalletContentClickIntents.kt | 92 ++++- .../WalletCurrencyActionsClickIntents.kt | 372 +++++++++++++++++- .../intents/WalletWarningsClickIntents.kt | 241 +++++++++++- 7 files changed, 934 insertions(+), 44 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt new file mode 100644 index 0000000000..7e62eb79e3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import kotlinx.coroutines.flow.* +import timber.log.Timber + +internal fun GetSelectedWalletSyncUseCase.unwrap(): UserWallet? { + return this().fold( + ifLeft = { + Timber.e("Impossible to get selected wallet $it") + null + }, + ifRight = { it }, + ) +} + +internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId: UserWalletId): CryptoCurrencyStatus? { + return this(userWalletId) + .conflate() + .distinctUntilChanged() + .filter(Either::isRight) + .firstOrNull() + ?.fold( + ifLeft = { + Timber.e("Impossible to get primary currency status $it") + null + }, + ifRight = { it }, + ) +} + +internal suspend fun GetSelectedAppCurrencyUseCase.unwrap(): AppCurrency { + return this() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .firstOrNull() + ?: AppCurrency.Default +} + +internal suspend fun GetPrimaryCurrencyStatusUpdatesUseCase.collectLatest( + userWalletId: UserWalletId, + onRight: suspend (CryptoCurrencyStatus) -> Unit, +) { + this(userWalletId = userWalletId) + .conflate() + .distinctUntilChanged() + .collectLatest { maybeStatus -> + maybeStatus.onRight { onRight(it) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt new file mode 100644 index 0000000000..d9be6984f5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/BaseWalletClickIntents.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import kotlinx.coroutines.CoroutineScope +import kotlin.properties.Delegates + +/** + * Base wallet click intents component. + * Provides router and viewModelScope to child classes. + * +[REDACTED_AUTHOR] + */ +@Suppress("UnnecessaryAbstractClass") +internal abstract class BaseWalletClickIntents { + + protected val router: InnerWalletRouter get() = _router + protected val viewModelScope: CoroutineScope get() = _viewModelScope + + private var _router: InnerWalletRouter by Delegates.notNull() + private var _viewModelScope: CoroutineScope by Delegates.notNull() + + open fun initialize(router: InnerWalletRouter, coroutineScope: CoroutineScope) { + _router = router + _viewModelScope = coroutineScope + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 29df476122..e7cc3d17a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -1,11 +1,20 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import com.tangem.core.navigation.AppScreen import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.UpdateWalletUseCase +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject -/** -[REDACTED_AUTHOR] - */ internal interface WalletCardClickIntents { fun onRenameClick(userWalletId: UserWalletId, name: String) @@ -15,17 +24,48 @@ internal interface WalletCardClickIntents { fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) } -internal class WalletCardClickIntentsImplementor @Inject constructor() : WalletCardClickIntents { +internal class WalletCardClickIntentsImplementor @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val walletEventSender: WalletEventSender, + // TODO: private val walletScreenContentLoader: WalletScreenContentLoader, + private val updateWalletUseCase: UpdateWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, + private val dispatchers: CoroutineDispatcherProvider, +) : BaseWalletClickIntents(), WalletCardClickIntents { override fun onRenameClick(userWalletId: UserWalletId, name: String) { - // TODO + viewModelScope.launch(dispatchers.main) { + updateWalletUseCase(userWalletId = userWalletId, update = { it.copy(name) }) + } } override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) { - // TODO + walletEventSender.send( + event = WalletEvent.ShowAlert( + state = WalletAlertState.RemoveWalletAlert( + onConfirmClick = { onDeleteAfterConfirmationClick(userWalletId) }, + ), + ), + ) } override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { - // TODO + viewModelScope.launch(dispatchers.main) { + // TODO: walletScreenContentLoader.cancel(userWalletId) + deleteWalletUseCase(userWalletId) + .onRight { popBackIfAllWalletsIsLocked() } + .onLeft { Timber.e(it.toString()) } + } + } + + private fun popBackIfAllWalletsIsLocked() { + val wallets = stateHolder.value.wallets.map(WalletState::walletCardState) + val unlockedWallet = wallets.count { it !is WalletCardState.LockedContent } + + if (unlockedWallet == 1) { + router.popBackStack( + screen = if (wallets.size > 1) AppScreen.Welcome else AppScreen.Home, + ) + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt index 4903e65c4d..643c5dec03 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt @@ -1,27 +1,150 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.settings.NeverToShowWalletsScrollPreview +import com.tangem.domain.tokens.FetchCardTokenListUseCase +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.tokens.FetchTokenListUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.SelectWalletUseCase +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetRefreshStateTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListErrorTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject +/** +[REDACTED_AUTHOR] + */ +@Suppress("LongParameterList") internal class WalletClickIntentsV2 @Inject constructor( private val walletCardClickIntentsImplementor: WalletCardClickIntentsImplementor, private val warningsClickIntentsImplementer: WalletWarningsClickIntentsImplementer, private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, -) : WalletCardClickIntents by walletCardClickIntentsImplementor, + private val stateHolder: WalletStateHolderV2, + // TODO: private val walletScreenContentLoader: WalletScreenContentLoader, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val selectWalletUseCase: SelectWalletUseCase, + // TODO: private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val fetchTokenListUseCase: FetchTokenListUseCase, + private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview, + private val analyticsEventHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, +) : BaseWalletClickIntents(), + WalletCardClickIntents by walletCardClickIntentsImplementor, WalletWarningsClickIntents by warningsClickIntentsImplementer, WalletCurrencyActionsClickIntents by currencyActionsClickIntentsImplementor, WalletContentClickIntents by contentClickIntentsImplementor { - @Suppress("UnusedPrivateMember") + override fun initialize(router: InnerWalletRouter, coroutineScope: CoroutineScope) { + super.initialize(router, coroutineScope) + + walletCardClickIntentsImplementor.initialize(router, coroutineScope) + warningsClickIntentsImplementer.initialize(router, coroutineScope) + currencyActionsClickIntentsImplementor.initialize(router, coroutineScope) + contentClickIntentsImplementor.initialize(router, coroutineScope) + } + fun onWalletChange(index: Int) { - // TODO + viewModelScope.launch(dispatchers.main) { + launch(dispatchers.main) { neverToShowWalletsScrollPreview() } + + val maybeUserWallet = selectWalletUseCase( + userWalletId = stateHolder.value.wallets[index].walletCardState.id, + ) + + stateHolder.update { it.copy(selectedWalletIndex = index) } + + maybeUserWallet.onRight { + // TODO: + // walletScreenContentLoader.load( + // userWallet = it, + // appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + // clickIntents = this@WalletClickIntentsV2, + // coroutineScope = viewModelScope, + // ) + } + } } fun onRefreshSwipe() { - // TODO + when (stateHolder.getSelectedWallet()) { + is WalletState.MultiCurrency.Content -> { + analyticsEventHandler.send(PortfolioEvent.Refreshed) + refreshMultiCurrencyContent() + } + is WalletState.SingleCurrency.Content -> { + analyticsEventHandler.send(PortfolioEvent.Refreshed) + refreshSingleCurrencyContent() + } + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + -> Unit + } + } + + private fun refreshMultiCurrencyContent() { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + ) + + viewModelScope.launch(dispatchers.main) { + val maybeFetchResult = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) + } else { + fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) + } + + maybeFetchResult.onLeft { + stateHolder.update(SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it)) + } + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), + ) + } } fun onReloadClick() { - // TODO + refreshSingleCurrencyContent() + } + + // FIXME: refreshSingleCurrencyContent mustn't update the TxHistory and Buttons. It only must fetch primary + // currency. Now it not works because GetPrimaryCurrency's subscriber uses .distinctUntilChanged() + private fun refreshSingleCurrencyContent() { + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + ) + + viewModelScope.launch(dispatchers.main) { + fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) + + // TODO: + // walletScreenContentLoader.load( + // userWallet = userWallet, + // appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + // clickIntents = this@WalletClickIntentsV2, + // coroutineScope = viewModelScope, + // isRefresh = true, + // ) + + stateHolder.update( + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), + ) + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 02fc91d677..82c943e41b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -1,7 +1,27 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.TokensAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.MultiWalletCurrencyActionsConverter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch import javax.inject.Inject internal interface WalletContentClickIntents { @@ -21,33 +41,81 @@ internal interface WalletContentClickIntents { fun onTransactionClick(txHash: String) } -internal class WalletContentClickIntentsImplementor @Inject constructor() : WalletContentClickIntents { +@Suppress("LongParameterList") +internal class WalletContentClickIntentsImplementor @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, + private val reduxStateHolder: ReduxStateHolder, +) : BaseWalletClickIntents(), WalletContentClickIntents { - override fun onBackClick() { - // TODO - } + override fun onBackClick() = router.popBackStack() - override fun onDetailsClick() { - // TODO - } + override fun onDetailsClick() = router.openDetailsScreen() override fun onManageTokensClick() { - // TODO + analyticsEventHandler.send(PortfolioEvent.ButtonManageTokens) + reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) + router.openManageTokensScreen() } override fun onOrganizeTokensClick() { - // TODO + analyticsEventHandler.send(PortfolioEvent.OrganizeTokens) + router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } override fun onTokenItemClick(currency: CryptoCurrency) { - // TODO + analyticsEventHandler.send(PortfolioEvent.TokenTapped) + router.openTokenDetails(stateHolder.getSelectedWalletId(), currency) } override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + viewModelScope.launch(dispatchers.main) { + getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus) + .take(count = 1) + .collectLatest { + showActionsBottomSheet(it, userWallet) + } + } + } + + private fun showActionsBottomSheet(tokenActionsState: TokenActionsState, userWallet: UserWallet) { + stateHolder.update( + OpenBottomSheetTransformer( + userWalletId = userWallet.walletId, + content = ActionsBottomSheetConfig( + actions = MultiWalletCurrencyActionsConverter( + userWallet = userWallet, + clickIntents = currencyActionsClickIntentsImplementor, + ).convert(tokenActionsState), + ), + onDismissBottomSheet = { + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = userWallet.walletId), + ) + }, + ), + ) } override fun onTransactionClick(txHash: String) { - // TODO + viewModelScope.launch(dispatchers.main) { + val currency = getPrimaryCurrencyStatusUpdatesUseCase.unwrap( + userWalletId = stateHolder.getSelectedWalletId(), + ) + ?.currency + ?: return@launch + + router.openUrl( + url = getExplorerTransactionUrlUseCase(txHash = txHash, networkId = currency.network.id), + ) + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index dd10ad4337..0b6070971a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -1,6 +1,46 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.address.AddressType +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.extensions.WrappedList +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase +import com.tangem.domain.tokens.RemoveCurrencyUseCase +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent +import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch import javax.inject.Inject interface WalletCurrencyActionsClickIntents { @@ -24,41 +64,353 @@ interface WalletCurrencyActionsClickIntents { fun onExploreClick() } -internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor() : WalletCurrencyActionsClickIntents { +@Suppress("LongParameterList", "LargeClass") +internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val walletEventSender: WalletEventSender, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val walletManagersFacade: WalletManagersFacade, + private val isDemoCardUseCase: IsDemoCardUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, + private val removeCurrencyUseCase: RemoveCurrencyUseCase, + private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, + private val getExploreUrlUseCase: GetExploreUrlUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, + private val reduxStateHolder: ReduxStateHolder, +) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonSend(cryptoCurrencyStatus.currency.symbol), + ) + + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId)) + + when (val currency = cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> sendCoin(cryptoCurrencyStatus, userWallet) + is CryptoCurrency.Token -> sendToken(currency, cryptoCurrencyStatus.value, userWallet) + } + } + + private fun sendCoin(cryptoCurrencyStatus: CryptoCurrencyStatus, userWallet: UserWallet) { + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendCoin(userWallet = userWallet, coinStatus = cryptoCurrencyStatus), + ) + } + + private fun sendToken( + cryptoCurrency: CryptoCurrency.Token, + cryptoCurrencyStatus: CryptoCurrencyStatus.Status, + userWallet: UserWallet, + ) { + viewModelScope.launch(dispatchers.main) { + getNetworkCoinStatusUseCase( + userWalletId = userWallet.walletId, + networkId = cryptoCurrency.network.id, + derivationPath = cryptoCurrency.network.derivationPath, + isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + ) + .take(count = 1) + .collectLatest { + it.onRight { coinStatus -> + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendToken( + userWallet = userWallet, + tokenCurrency = cryptoCurrency, + tokenFiatRate = cryptoCurrencyStatus.fiatRate, + coinFiatRate = coinStatus.value.fiatRate, + ), + ) + } + } + } } override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + val userWalletId = stateHolder.getSelectedWalletId() + + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrencyStatus.currency.symbol), + ) + + viewModelScope.launch(dispatchers.main) { + val currency = cryptoCurrencyStatus.currency + val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) + + analyticsEventHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened) + + stateHolder.update( + OpenBottomSheetTransformer( + userWalletId = userWalletId, + content = createReceiveBottomSheetContent(currency, addresses), + onDismissBottomSheet = { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + }, + ), + ) + } + } + + private fun createReceiveBottomSheetContent( + currency: CryptoCurrency, + addresses: List
, + ): TangemBottomSheetConfigContent { + return TokenReceiveBottomSheetConfig( + name = currency.name, + symbol = currency.symbol, + network = currency.network.name, + addresses = addresses.map { address -> + AddressModel( + value = address.value, + type = AddressModel.Type.valueOf(address.type.name), + ) + }, + onCopyClick = { + analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) + }, + onShareClick = { + analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) + }, + ) } override fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonCopyAddress(cryptoCurrencyStatus.currency.symbol), + ) + + viewModelScope.launch(dispatchers.main) { + walletManagersFacade.getAddress( + userWalletId = stateHolder.getSelectedWalletId(), + network = cryptoCurrencyStatus.currency.network, + ) + .find { it.type == AddressType.Default } + ?.value + ?.let { + walletEventSender.send( + event = WalletEvent.CopyAddress( + address = it, + toast = resourceReference(R.string.wallet_notification_address_copied), + ), + ) + } + } } override fun onHideTokensClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrencyStatus.currency.symbol), + ) + + viewModelScope.launch(dispatchers.main) { + walletEventSender.send( + event = WalletEvent.ShowAlert( + state = getHideTokeAlertConfig(stateHolder.getSelectedWalletId(), cryptoCurrencyStatus), + ), + ) + } + } + + private suspend fun getHideTokeAlertConfig( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): WalletAlertState.DefaultAlert { + val currency = cryptoCurrencyStatus.currency + return if (currency is CryptoCurrency.Coin && !isCryptoCurrencyCoinCouldHide(userWalletId, currency)) { + WalletAlertState.DefaultAlert( + title = resourceReference( + id = R.string.token_details_unable_hide_alert_title, + formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), + ), + message = resourceReference( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = WrappedList( + listOf( + cryptoCurrencyStatus.currency.name, + cryptoCurrencyStatus.currency.network.name, + ), + ), + ), + onConfirmClick = null, + ) + } else { + WalletAlertState.DefaultAlert( + title = resourceReference( + id = R.string.token_details_hide_alert_title, + formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), + ), + message = resourceReference(R.string.token_details_hide_alert_message), + onConfirmClick = { onPerformHideToken(cryptoCurrencyStatus) }, + ) + } } override fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + val userWalletId = stateHolder.getSelectedWalletId() + + viewModelScope.launch(dispatchers.io) { + removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency) + .fold( + ifLeft = { + walletEventSender.send( + event = WalletEvent.ShowToast(text = resourceReference(R.string.common_error)), + ) + }, + ifRight = { + getSelectedWalletSyncUseCase.unwrap()?.let { userWallet -> + reduxStateHolder.dispatch( + action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet), + ) + } + + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + }, + ) + } } override fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrencyStatus.currency.symbol), + ) + + showErrorIfDemoModeOrElse { + viewModelScope.launch(dispatchers.main) { + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.Sell( + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, + ), + ) + } + } } override fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrencyStatus.currency.symbol), + ) + + showErrorIfDemoModeOrElse { + viewModelScope.launch(dispatchers.main) { + reduxStateHolder.dispatch( + TradeCryptoAction.New.Buy( + userWallet = userWallet, + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, + ), + ) + } + } } override fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO + analyticsEventHandler.send( + event = TokenScreenAnalyticsEvent.ButtonExchange(cryptoCurrencyStatus.currency.symbol), + ) + + reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrencyStatus.currency)) } override fun onExploreClick() { - // TODO + showErrorIfDemoModeOrElse(action = ::openExplorer) + } + + private fun openExplorer() { + val userWalletId = stateHolder.getSelectedWalletId() + + viewModelScope.launch(dispatchers.main) { + val currency = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId)?.currency ?: return@launch + val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) + + if (addresses.size == 1) { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = currency, + addressType = AddressType.Default, + ), + ) + } else { + showChooseAddressBottomSheet(userWalletId, addresses, currency) + } + } + } + + private fun showChooseAddressBottomSheet( + userWalletId: UserWalletId, + addresses: List
, + currency: CryptoCurrency, + ) { + stateHolder.update( + OpenBottomSheetTransformer( + userWalletId = userWalletId, + content = ChooseAddressBottomSheetConfig( + addressModels = addresses + .map { address -> + AddressModel( + value = address.value, + type = AddressModel.Type.valueOf(address.type.name), + ) + } + .toImmutableList(), + onClick = { + onAddressTypeSelected( + userWalletId = userWalletId, + currency = currency, + addressModel = it, + ) + }, + ), + onDismissBottomSheet = { + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = userWalletId), + ) + }, + ), + ) + } + + private fun onAddressTypeSelected( + userWalletId: UserWalletId, + currency: CryptoCurrency, + addressModel: AddressModel, + ) { + viewModelScope.launch(dispatchers.main) { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = currency, + addressType = AddressType.valueOf(addressModel.type.name), + ), + ) + + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = userWalletId), + ) + } + } + + private fun showErrorIfDemoModeOrElse(action: () -> Unit) { + val cardId = getSelectedWalletSyncUseCase.unwrap()?.cardId ?: return + + if (isDemoCardUseCase(cardId = cardId)) { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) + + walletEventSender.send( + event = WalletEvent.ShowError( + text = resourceReference(id = R.string.alert_demo_feature_disabled), + ), + ) + } else { + action() + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 99dfe53ffb..b4b9b2d06f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -1,6 +1,44 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import com.tangem.blockchain.blockchains.cardano.CardanoUtils +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.toMapKey +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.card.SetCardWasScannedUseCase +import com.tangem.domain.common.configs.CardConfig +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.LegacyAction +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.NeverToSuggestRateAppUseCase +import com.tangem.domain.settings.RemindToRateAppLaterUseCase +import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UnlockWalletsError +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase +import com.tangem.domain.wallets.usecase.UpdateWalletUseCase +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen +import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler +import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletError +import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.launch import javax.inject.Inject internal interface WalletWarningsClickIntents { @@ -24,41 +62,224 @@ internal interface WalletWarningsClickIntents { fun onCloseRateAppWarningClick() } -internal class WalletWarningsClickIntentsImplementer @Inject constructor() : WalletWarningsClickIntents { +@Suppress("LongParameterList") +internal class WalletWarningsClickIntentsImplementer @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val walletEventSender: WalletEventSender, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val updateWalletUseCase: UpdateWalletUseCase, + private val unlockWalletsUseCase: UnlockWalletsUseCase, + private val derivePublicKeysUseCase: DerivePublicKeysUseCase, + private val scanCardToUnlockWalletClickHandler: ScanCardToUnlockWalletClickHandler, + private val fetchTokenListUseCase: FetchTokenListUseCase, + private val setCardWasScannedUseCase: SetCardWasScannedUseCase, + private val neverToSuggestRateAppUseCase: NeverToSuggestRateAppUseCase, + private val remindToRateAppLaterUseCase: RemindToRateAppLaterUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + private val reduxStateHolder: ReduxStateHolder, + private val dispatchers: CoroutineDispatcherProvider, +) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { - // TODO + analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped) + router.openOnboardingScreen() } override fun onCloseAlreadySignedHashesWarningClick() { - // TODO + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + viewModelScope.launch(dispatchers.main) { + setCardWasScannedUseCase(cardId = userWallet.cardId) + } } override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List) { - // TODO + val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return + + analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main)) + analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped) + + viewModelScope.launch(dispatchers.main) { + deriveMissingCurrencies( + scanResponse = userWallet.scanResponse, + currencyList = missedAddressCurrencies, + ) { scannedCardResponse -> + updateWalletUseCase( + userWalletId = userWallet.walletId, + update = { it.copy(scanResponse = scannedCardResponse) }, + ) + .onRight { fetchTokenListUseCase(userWalletId = it.walletId) } + } + } } + // TODO: [REDACTED_JIRA] + private fun deriveMissingCurrencies( + scanResponse: ScanResponse, + currencyList: List, + onSuccess: suspend (ScanResponse) -> Unit, + ) { + val config = CardConfig.createConfig(scanResponse.card) + val derivationDataList = currencyList.mapNotNull { + config.primaryCurve(blockchain = Blockchain.fromId(it.network.id.value))?.let { curve -> + getNewDerivations(curve, scanResponse, it) + } + } + + val derivations = buildMap> { + derivationDataList.forEach { + val current = this[it.derivations.first] + if (current != null) { + current.addAll(it.derivations.second) + current.distinct() + } else { + this[it.derivations.first] = it.derivations.second.toMutableList() + } + } + }.ifEmpty { return } + + viewModelScope.launch(dispatchers.io) { + derivePublicKeysUseCase(cardId = null, derivations = derivations) + .onRight { + val newDerivedKeys = it.entries + val oldDerivedKeys = scanResponse.derivedKeys + + val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() + + val updatedDerivedKeys = walletKeys.associateWith { walletKey -> + val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap()) + val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) + ExtendedPublicKeysMap(oldDerivations + newDerivations) + } + val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys) + + onSuccess(updatedScanResponse) + } + } + } + + private fun getNewDerivations( + curve: EllipticCurve, + scanResponse: ScanResponse, + currency: CryptoCurrency, + ): DerivationData? { + val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null + + val blockchain = Blockchain.fromId(currency.network.id.value) + val supportedCurves = blockchain.getSupportedCurves() + val path = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) + .takeIf { supportedCurves.contains(curve) } + + val customPath = currency.network.derivationPath.value?.let { + DerivationPath(it) + }.takeIf { supportedCurves.contains(curve) } + + val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList() + if (bothCandidates.isEmpty()) return null + + if (currency is CryptoCurrency.Coin && blockchain == Blockchain.Cardano) { + currency.network.derivationPath.value?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() + val alreadyDerivedKeys: ExtendedPublicKeysMap = + scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) + val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() + + val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } + if (toDerive.isEmpty()) return null + + return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) + } + + class DerivationData(val derivations: Pair>) + override fun onOpenUnlockWalletsBottomSheetClick() { - // TODO + stateHolder.update( + OpenBottomSheetTransformer( + content = requireNotNull(stateHolder.getSelectedWallet().bottomSheetConfig).content, + userWalletId = stateHolder.getSelectedWalletId(), + onDismissBottomSheet = { + stateHolder.update( + CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId()), + ) + }, + ), + ) } override fun onUnlockWalletClick() { - // TODO + analyticsEventHandler.send(MainScreen.NoticeWalletLocked) + + viewModelScope.launch(dispatchers.main) { + unlockWalletsUseCase(throwIfNotAllWalletsUnlocked = true) + .onRight { stateHolder.update(CloseBottomSheetTransformer(stateHolder.getSelectedWalletId())) } + .onLeft(::handleUnlockWalletsError) + } + } + + private fun handleUnlockWalletsError(error: UnlockWalletsError) { + val event = when (error) { + is UnlockWalletsError.DataError, + is UnlockWalletsError.UnableToUnlockWallets, + -> WalletEvent.ShowToast(resourceReference(R.string.user_wallet_list_error_unable_to_unlock)) + is UnlockWalletsError.NoUserWalletSelected, + is UnlockWalletsError.NotAllUserWalletsUnlocked, + -> WalletEvent.ShowAlert(WalletAlertState.RescanWallets) + } + + walletEventSender.send(event) } override fun onScanToUnlockWalletClick() { - // TODO + analyticsEventHandler.send(event = MainScreen.WalletUnlockTapped) + + viewModelScope.launch(dispatchers.main) { + scanCardToUnlockWalletClickHandler(walletId = stateHolder.getSelectedWalletId()) + .onLeft { error -> + when (error) { + ScanCardToUnlockWalletError.WrongCardIsScanned -> { + walletEventSender.send( + event = WalletEvent.ShowAlert(WalletAlertState.WrongCardIsScanned), + ) + } + ScanCardToUnlockWalletError.ManyScanFails -> router.openScanFailedDialog() + } + } + } } override fun onLikeAppClick() { - // TODO + analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked)) + + walletEventSender.send( + event = WalletEvent.RateApp( + onDismissClick = { + viewModelScope.launch(dispatchers.main) { + neverToSuggestRateAppUseCase() + } + }, + ), + ) } override fun onDislikeAppClick() { - // TODO + analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked)) + + viewModelScope.launch(dispatchers.main) { + neverToSuggestRateAppUseCase() + + reduxStateHolder.dispatch(LegacyAction.SendEmailRateCanBeBetter) + } } override fun onCloseRateAppWarningClick() { - // TODO + analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Closed)) + + viewModelScope.launch(dispatchers.main) { + remindToRateAppLaterUseCase() + } } } \ No newline at end of file From 807233e9097e5a183d7516d0e852946308326d54 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 11:32:59 +0200 Subject: [PATCH 064/139] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 1 - features/swap/domain/build.gradle.kts | 2 + .../feature/swap/domain/SwapInteractor.kt | 9 +- .../feature/swap/domain/SwapInteractorImpl.kt | 83 ++++++++++++++++--- .../swap/domain/di/SwapDomainModule.kt | 21 +++++ .../feature/swap/viewmodels/SwapViewModel.kt | 10 ++- 6 files changed, 106 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 8dfb19bd16..ead5fca293 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -36,7 +36,6 @@ internal object TransactionDomainModule { isDemoCardUseCase = isDemoCardUseCase, cardSdkConfigRepository = cardSdkConfigRepository, walletManagersFacade = walletManagersFacade, - ) } } \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 00bcf936c1..4f4d42fe13 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -25,6 +25,8 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.transaction) implementation(projects.domain.legacy) + implementation(projects.domain.demo) + implementation(projects.domain.card) /** Core modules */ implementation(projects.core.utils) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 1057373c25..aa76195519 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* @@ -68,11 +69,11 @@ interface SwapInteractor { @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun onSwap( - exchangeProviderType: ExchangeProviderType, + swapProvider: SwapProvider, networkId: String, - swapData: SwapDataModel, - currencyToSend: CryptoCurrency, - currencyToGet: CryptoCurrency, + swapData: SwapDataModel?, + currencyToSend: CryptoCurrencyStatus, + currencyToGet: CryptoCurrencyStatus, amountToSwap: String, fee: TxFee, ): TxState diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 7b8944fe0a..9d8e7109d5 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse +import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase @@ -12,9 +13,13 @@ import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.cache.SwapDataCache import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter @@ -50,6 +55,7 @@ internal class SwapInteractorImpl @Inject constructor( private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val walletManagersFacade: WalletManagersFacade, + private val sendTransactionUseCase: SendTransactionUseCase, private val quotesRepository: QuotesRepository, private val dispatcher: CoroutineDispatcherProvider, ) : SwapInteractor { @@ -359,24 +365,34 @@ internal class SwapInteractorImpl @Inject constructor( @Deprecated("used in old swap mechanism") override suspend fun onSwap( - exchangeProviderType: ExchangeProviderType, + swapProvider: SwapProvider, networkId: String, - swapData: SwapDataModel, - currencyToSend: CryptoCurrency, - currencyToGet: CryptoCurrency, + swapData: SwapDataModel?, + currencyToSend: CryptoCurrencyStatus, + currencyToGet: CryptoCurrencyStatus, amountToSwap: String, fee: TxFee, ): TxState { - return when (exchangeProviderType) { + return when (swapProvider.type) { ExchangeProviderType.CEX -> { - onSwapCex() + val amountDecimal = toBigDecimalOrNull(amountToSwap) + val amount = SwapAmount(requireNotNull(amountDecimal), getTokenDecimals(currencyToSend.currency)) + + onSwapCex( + currencyToSend = currencyToSend, + currencyToGet = currencyToGet, + amount = amount, + fee = fee, + providerId = swapProvider.providerId, + userWalletId = requireNotNull(getSelectedWallet()).walletId + ) } ExchangeProviderType.DEX -> { onSwapDex( networkId = networkId, - swapData = swapData, - currencyToSend = currencyToSend, - currencyToGet = currencyToGet, + swapData = requireNotNull(swapData), + currencyToSend = currencyToSend.currency, + currencyToGet = currencyToGet.currency, amountToSwap = amountToSwap, fee = fee, ) @@ -468,8 +484,53 @@ internal class SwapInteractorImpl @Inject constructor( } } - private fun onSwapCex(): TxState { - TODO() + private suspend fun onSwapCex( + currencyToSend: CryptoCurrencyStatus, + currencyToGet: CryptoCurrencyStatus, + amount: SwapAmount, + fee: TxFee, + providerId: String, + userWalletId: UserWalletId + ): TxState { + val exchangeData = repository.getExchangeData( + fromContractAddress = currencyToSend.currency.getContractAddress(), + fromNetwork = currencyToSend.currency.network.backendId, + toContractAddress = currencyToGet.currency.getContractAddress(), + toNetwork = currencyToGet.currency.network.backendId, + fromAmount = amount.toStringWithRightOffset(), + fromDecimals = amount.decimals, + providerId = providerId, + rateType = RateType.FLOAT, + toAddress = currencyToGet.value.networkAddress?.defaultAddress ?: "", + ) + + // val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } + + val txData = walletManagersFacade.createTransaction( + amount = amount.value.convertToAmount(currencyToSend.currency), + fee = Fee.Common(fee.feeValue.convertToAmount(currencyToSend.currency)), + memo = null, + destination = (exchangeData.dataModel?.transaction as ExpressTransactionModel.CEX).txTo, + userWalletId = userWalletId, + network = currencyToSend.currency.network, + ) + + val result = sendTransactionUseCase( + requireNotNull(txData), + userWallet = requireNotNull(getSelectedWallet()), + network = currencyToSend.currency.network + ) + + return result.fold(ifLeft = { + TxState.UnknownError + }, ifRight = { + TxState.TxSent( + txAddress = userWalletManager.getLastTransactionHash( + networkId = currencyToSend.currency.network.backendId, + derivationPath = derivationPath + ) ?: "" + ) + }) } @Deprecated("used in old swap mechanism") diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 4be138a37f..58eec2d29c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -1,10 +1,14 @@ package com.tangem.feature.swap.domain.di +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -36,6 +40,7 @@ class SwapDomainModule { walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + @SwapScope sendTransactionUseCase: SendTransactionUseCase, quotesRepository: QuotesRepository, walletManagersFacade: WalletManagersFacade, coroutineDispatcherProvider: CoroutineDispatcherProvider, @@ -51,6 +56,7 @@ class SwapDomainModule { walletFeatureToggles = walletFeatureToggles, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, + sendTransactionUseCase = sendTransactionUseCase, quotesRepository = quotesRepository, walletManagersFacade = walletManagersFacade, dispatcher = coroutineDispatcherProvider, @@ -105,6 +111,21 @@ class SwapDomainModule { dispatchers = dispatchers, ) } + + @SwapScope + @Provides + @Singleton + fun provideSendTransactionUseCase( + walletManagersFacade: WalletManagersFacade, + cardSdkConfigRepository: CardSdkConfigRepository, + ): SendTransactionUseCase { + return SendTransactionUseCase( + isDemoCardUseCase = IsDemoCardUseCase(DemoConfig()), + cardSdkConfigRepository = cardSdkConfigRepository, + walletManagersFacade = walletManagersFacade + ) + } + } @Qualifier diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 50c84c5b47..d3121006ab 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -17,6 +17,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider @@ -346,11 +347,11 @@ internal class SwapViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { swapInteractor.onSwap( - exchangeProviderType = requireNotNull(dataState.selectedProvider?.type), + swapProvider = requireNotNull(dataState.selectedProvider), networkId = dataState.networkId, - swapData = requireNotNull(dataState.swapDataModel), - currencyToSend = requireNotNull(dataState.fromCryptoCurrency?.currency), - currencyToGet = requireNotNull(dataState.toCryptoCurrency?.currency), + swapData = dataState.swapDataModel, + currencyToSend = requireNotNull(dataState.fromCryptoCurrency), + currencyToGet = requireNotNull(dataState.toCryptoCurrency), amountToSwap = requireNotNull(dataState.amount), fee = requireNotNull(dataState.selectedFee), ) @@ -389,6 +390,7 @@ internal class SwapViewModel @Inject constructor( } } .onFailure { + Timber.e(it) startLoadingQuotesFromLastState() makeDefaultAlert() } From 1712785ec62efe5f009ba4be8c51155139d3b163 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 11:47:34 +0200 Subject: [PATCH 065/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 27 ++++++++++++++++--- .../swap/domain/di/SwapDomainModule.kt | 9 ++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 9d8e7109d5..1f0095a373 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -504,8 +504,6 @@ internal class SwapInteractorImpl @Inject constructor( toAddress = currencyToGet.value.networkAddress?.defaultAddress ?: "", ) - // val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } - val txData = walletManagersFacade.createTransaction( amount = amount.value.convertToAmount(currencyToSend.currency), fee = Fee.Common(fee.feeValue.convertToAmount(currencyToSend.currency)), @@ -521,9 +519,32 @@ internal class SwapInteractorImpl @Inject constructor( network = currencyToSend.currency.network ) + return result.fold(ifLeft = { - TxState.UnknownError + when(it){ + is SendTransactionError.NetworkError -> TxState.NetworkError + is SendTransactionError.DataError -> TxState.BlockchainError + SendTransactionError.DemoCardError -> TxState.UnknownError + else -> TxState.UnknownError + } + }, ifRight = { + if (walletFeatureToggles.isRedesignedScreenEnabled) { + onSuccessNewFlow(currencyToGet.currency) + } else { + onSuccessLegacyFlow(currencyToGet.currency) + } + TxState.TxSent( + fromAmount = amountFormatter.formatSwapAmountToUI( + amount, + currencyToSend.currency.symbol, + ), + toAmount = amountFormatter.formatSwapAmountToUI( + exchangeData.dataModel.toTokenAmount, + currencyToGet.currency.symbol, + ), + txAddress = userWalletManager.getLastTransactionHash(currencyToSend.currency.network.backendId, derivationPath) ?: "", + ) TxState.TxSent( txAddress = userWalletManager.getLastTransactionHash( networkId = currencyToSend.currency.network.backendId, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 58eec2d29c..9c06a31f85 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -112,15 +112,22 @@ class SwapDomainModule { ) } + @SwapScope + @Provides + fun provideDemoCardUseCase() : IsDemoCardUseCase { + return IsDemoCardUseCase(config = DemoConfig()) + } + @SwapScope @Provides @Singleton fun provideSendTransactionUseCase( + @SwapScope isDemoCardUseCase: IsDemoCardUseCase, walletManagersFacade: WalletManagersFacade, cardSdkConfigRepository: CardSdkConfigRepository, ): SendTransactionUseCase { return SendTransactionUseCase( - isDemoCardUseCase = IsDemoCardUseCase(DemoConfig()), + isDemoCardUseCase = isDemoCardUseCase, cardSdkConfigRepository = cardSdkConfigRepository, walletManagersFacade = walletManagersFacade ) From c026d65fba49a50dc320c74dd1945f49d65ccb57 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 12:08:19 +0200 Subject: [PATCH 066/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 1f0095a373..15bcdad9af 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -459,11 +459,6 @@ internal class SwapInteractorImpl @Inject constructor( ) return when (result) { is SendTxResult.Success -> { - if (walletFeatureToggles.isRedesignedScreenEnabled) { - onSuccessNewFlow(currencyToGet) - } else { - onSuccessLegacyFlow(currencyToGet) - } TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, @@ -529,11 +524,6 @@ internal class SwapInteractorImpl @Inject constructor( } }, ifRight = { - if (walletFeatureToggles.isRedesignedScreenEnabled) { - onSuccessNewFlow(currencyToGet.currency) - } else { - onSuccessLegacyFlow(currencyToGet.currency) - } TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, @@ -568,24 +558,6 @@ internal class SwapInteractorImpl @Inject constructor( return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId) } - @Deprecated("used in old swap mechanism") - private suspend fun onSuccessLegacyFlow(currency: CryptoCurrency) { - userWalletManager.addToken(swapCurrencyConverter.convert(currency), derivationPath) - userWalletManager.refreshWallet() - } - - @Deprecated("used in old swap mechanism") - private suspend fun onSuccessNewFlow(currency: CryptoCurrency) { - getSelectedWalletSyncUseCase().fold( - ifRight = { userWallet -> - addCryptoCurrenciesUseCase(userWallet.walletId, currency) - }, - ifLeft = { - Timber.e("Swap Error on getSelectedWalletUseCase") - }, - ) - } - @Deprecated("used in old swap mechanism") private fun getTangemFee(): Double { return repository.getTangemFee() From 5384cf75ad15f558c2e7119c8caea2d9f604ad0c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 15:13:54 +0800 Subject: [PATCH 067/139] Updated on 2026-08-14 --- .../utils/TokenListAnalyticsSender.kt | 62 ++++++ .../domain/GetMultiWalletWarningsFactory.kt | 203 ++++++++++++++++++ .../domain/GetSingleWalletWarningsFactory.kt | 184 ++++++++++++++++ .../wallet/domain/WalletWithFundsChecker.kt | 38 ++++ .../MultiWalletWarningsSubscriber.kt | 37 ++++ .../subscribers/PrimaryCurrencySubscriber.kt | 92 ++++++++ .../SingleWalletButtonsSubscriber.kt | 45 ++++ .../SingleWalletNotificationsSubscriber.kt | 40 ++++ .../SingleWalletWithTokenListSubscriber.kt | 60 ++++++ .../wallet/subscribers/TokenListSubscriber.kt | 62 ++++++ .../wallet/subscribers/TxHistorySubscriber.kt | 108 ++++++++++ .../wallet/subscribers/WalletSubscriber.kt | 30 +++ 12 files changed, 961 insertions(+) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt new file mode 100644 index 0000000000..eb62eaf6e3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.wallet.presentation.wallet.analytics.utils + +import arrow.core.Either +import com.tangem.common.extensions.isZero +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import dagger.hilt.android.scopes.ViewModelScoped +import java.math.BigDecimal +import javax.inject.Inject + +@ViewModelScoped +internal class TokenListAnalyticsSender @Inject constructor( + private val analyticsEventHandler: AnalyticsEventHandler, +) { + + fun send(maybeTokenList: Either) { + val tokenList = (maybeTokenList as? Either.Right)?.value ?: return + + createCardBalanceState(tokenList)?.let { + analyticsEventHandler.send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balance = it)) + } + } + + private fun createCardBalanceState(tokenList: TokenList): AnalyticsParam.CardBalanceState? { + return when (val fiatBalance = tokenList.totalFiatBalance) { + is TokenList.FiatBalance.Failed -> fiatBalance.toCardBalanceState(tokenList) + is TokenList.FiatBalance.Loaded -> fiatBalance.toCardBalanceState() + TokenList.FiatBalance.Loading -> null + } + } + + private fun TokenList.FiatBalance.Failed.toCardBalanceState(tokenList: TokenList): AnalyticsParam.CardBalanceState { + val currenciesStatuses = when (tokenList) { + is TokenList.Empty -> emptyList() + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) + is TokenList.Ungrouped -> tokenList.currencies + } + + return when { + currenciesStatuses.isEmpty() -> AnalyticsParam.CardBalanceState.Empty + currenciesStatuses.any { it.value is CryptoCurrencyStatus.NoQuote } -> { + AnalyticsParam.CardBalanceState.NoRate + } + else -> AnalyticsParam.CardBalanceState.BlockchainError + } + } + + private fun TokenList.FiatBalance.Loaded.toCardBalanceState(): AnalyticsParam.CardBalanceState? { + return if (amount > BigDecimal.ZERO) { + AnalyticsParam.CardBalanceState.Full + } else if (amount.isZero()) { + AnalyticsParam.CardBalanceState.Empty + } else { + null + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt new file mode 100644 index 0000000000..83355647bd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -0,0 +1,203 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import arrow.core.Either +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flowOf +import timber.log.Timber +import javax.inject.Inject + +@ViewModelScoped +internal class GetMultiWalletWarningsFactory @Inject constructor( + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getTokenListUseCase: GetTokenListUseCase, + private val isDemoCardUseCase: IsDemoCardUseCase, + private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, + private val isNeedToBackupUseCase: IsNeedToBackupUseCase, +) { + + private var readyForRateAppNotification = false + + fun create(clickIntents: WalletClickIntentsV2): Flow> { + val userWallet = getSelectedWalletSyncUseCase().fold( + ifLeft = { + Timber.e("Failed to get selected wallet $it") + return flowOf(value = persistentListOf()) + }, + ifRight = { it }, + ) + + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + + return combine( + flow = getTokenListUseCase(userWallet.walletId).conflate(), + flow2 = isReadyToShowRateAppUseCase().conflate(), + flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), + // flow4 = getMissedAddressCryptoCurrenciesUseCase(userWallet.walletId).conflate(), + ) { maybeTokenList, isReadyToShowRating, isNeedToBackup -> + // maybeTokenList.onRight { Timber.e(it.toString()) } + // maybeMissedAddressCurrencies.onRight { Timber.e(it.toString()) } + readyForRateAppNotification = true + buildList { + addCriticalNotifications(cardTypesResolver) + + addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents) + + addWarningNotifications(cardTypesResolver, maybeTokenList, isNeedToBackup, clickIntents) + + addRateTheAppNotification(isReadyToShowRating, clickIntents) + }.toImmutableList() + } + } + + private fun MutableList.addCriticalNotifications(cardTypesResolver: CardTypesResolver) { + addIf( + element = WalletNotification.Critical.DevCard, + condition = !cardTypesResolver.isReleaseFirmwareType(), + ) + + addIf( + element = WalletNotification.Critical.FailedCardValidation, + condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(), + ) + + cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures -> + addIf( + element = WalletNotification.Warning.LowSignatures(count = remainingSignatures), + condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT, + ) + } + } + + private fun MutableList.addInformationalNotifications( + cardTypesResolver: CardTypesResolver, + maybeTokenList: Either, + clickIntents: WalletClickIntentsV2, + ) { + addIf( + element = WalletNotification.Informational.DemoCard, + condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), + ) + + addMissingAddressesNotification(maybeTokenList, clickIntents) + } + + private fun MutableList.addMissingAddressesNotification( + maybeTokenList: Either, + clickIntents: WalletClickIntentsV2, + ) { + val currencies = maybeTokenList.getMissingAddressCurrencies() + + addIf( + element = WalletNotification.Informational.MissingAddresses( + missingAddressesCount = currencies.count(), + onGenerateClick = { + clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = currencies) + }, + ), + condition = currencies.isNotEmpty(), + ) + } + + private fun Either.getMissingAddressCurrencies(): List { + return fold( + ifLeft = { emptyList() }, + ifRight = { tokenList -> + val currencies = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) + is TokenList.Ungrouped -> tokenList.currencies + is TokenList.Empty -> emptyList() + } + + currencies + .filter { it.value is CryptoCurrencyStatus.MissedDerivation } + .map(CryptoCurrencyStatus::currency) + }, + ) + } + + private fun MutableList.addWarningNotifications( + cardTypesResolver: CardTypesResolver, + tokenList: Either, + isNeedToBackup: Boolean, + clickIntents: WalletClickIntentsV2, + ) { + addIf( + element = WalletNotification.Warning.MissingBackup( + onStartBackupClick = clickIntents::onAddBackupCardClick, + ), + condition = isNeedToBackup, + ) + + addIf( + element = WalletNotification.Warning.TestNetCard, + condition = cardTypesResolver.isTestCard(), + ) + + addIf( + element = WalletNotification.Warning.SomeNetworksUnreachable, + condition = tokenList.hasUnreachableNetworks(), + ) + } + + private fun Either.hasUnreachableNetworks(): Boolean { + return fold( + ifLeft = { false }, + ifRight = { tokenList -> + val currencies = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) + is TokenList.Ungrouped -> tokenList.currencies + is TokenList.Empty -> emptyList() + } + + currencies.any { it.value is CryptoCurrencyStatus.Unreachable } + }, + ) + } + + private fun MutableList.addRateTheAppNotification( + isReadyToShowRating: Boolean, + clickIntents: WalletClickIntentsV2, + ) { + addIf( + element = WalletNotification.RateApp( + onLikeClick = clickIntents::onLikeAppClick, + onDislikeClick = clickIntents::onDislikeAppClick, + onCloseClick = clickIntents::onCloseRateAppWarningClick, + ), + condition = isReadyToShowRating && readyForRateAppNotification, + ) + } + + private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { + if (condition) { + add(element = element) + if (element is WalletNotification.Critical || element is WalletNotification.Warning) { + readyForRateAppNotification = false + } + } + } + + private companion object { + const val MAX_REMAINING_SIGNATURES_COUNT = 10 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt new file mode 100644 index 0000000000..a248e07dbe --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -0,0 +1,184 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import arrow.core.Either +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import timber.log.Timber +import javax.inject.Inject + +@ViewModelScoped +internal class GetSingleWalletWarningsFactory @Inject constructor( + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val isDemoCardUseCase: IsDemoCardUseCase, + private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, + private val isNeedToBackupUseCase: IsNeedToBackupUseCase, + private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, +) { + + private var readyForRateAppNotification = false + + fun create(clickIntents: WalletClickIntentsV2): Flow> { + val userWallet = getSelectedWalletSyncUseCase().fold( + ifLeft = { + Timber.e("Failed to get selected wallet $it") + return flowOf(value = persistentListOf()) + }, + ifRight = { it }, + ) + + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + + return combine( + flow = getPrimaryCurrencyStatusUpdatesUseCase(userWallet.walletId), + flow2 = isReadyToShowRateAppUseCase().conflate(), + flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), + ) { primaryCurrencyStatus, isReadyToShowRating, isNeedToBackup -> + readyForRateAppNotification = true + buildList { + addCriticalNotifications(cardTypesResolver) + + addInformationalNotifications(cardTypesResolver) + + addWarningNotifications( + userWallet, + cardTypesResolver, + primaryCurrencyStatus, + isNeedToBackup, + clickIntents, + ) + + addRateTheAppNotification(isReadyToShowRating, clickIntents) + }.toImmutableList() + } + } + + private fun MutableList.addCriticalNotifications(cardTypesResolver: CardTypesResolver) { + addIf( + element = WalletNotification.Critical.DevCard, + condition = !cardTypesResolver.isReleaseFirmwareType(), + ) + + addIf( + element = WalletNotification.Critical.FailedCardValidation, + condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(), + ) + + cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures -> + addIf( + element = WalletNotification.Warning.LowSignatures(count = remainingSignatures), + condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT, + ) + } + } + + private fun MutableList.addInformationalNotifications(cardTypesResolver: CardTypesResolver) { + addIf( + element = WalletNotification.Informational.DemoCard, + condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), + ) + } + + private suspend fun MutableList.addWarningNotifications( + userWallet: UserWallet, + cardTypesResolver: CardTypesResolver, + maybePrimaryCurrencyStatus: Either, + isNeedToBackup: Boolean, + clickIntents: WalletClickIntentsV2, + ) { + val cryptoCurrencyStatus = maybePrimaryCurrencyStatus.fold(ifLeft = { null }, ifRight = { it }) + + addIf( + element = WalletNotification.Warning.MissingBackup( + onStartBackupClick = clickIntents::onAddBackupCardClick, + ), + condition = isNeedToBackup, + ) + + addIf( + element = WalletNotification.Warning.TestNetCard, + condition = cardTypesResolver.isTestCard(), + ) + + addIf( + element = WalletNotification.Warning.NetworksUnreachable, + condition = cryptoCurrencyStatus?.value is CryptoCurrencyStatus.Unreachable, + ) + + addNoAccountWarning(cryptoCurrencyStatus) + + addIf( + element = WalletNotification.Warning.NumberOfSignedHashesIncorrect( + onCloseClick = clickIntents::onCloseAlreadySignedHashesWarningClick, + ), + condition = hasSignedHashes(userWallet, cryptoCurrencyStatus), + ) + } + + private fun MutableList.addNoAccountWarning(cryptoCurrencyStatus: CryptoCurrencyStatus?) { + val noAccountStatus = cryptoCurrencyStatus?.value as? CryptoCurrencyStatus.NoAccount + if (noAccountStatus != null) { + add( + element = WalletNotification.Informational.NoAccount( + network = cryptoCurrencyStatus.currency.name, + amount = noAccountStatus.amountToCreateAccount.toString(), + symbol = cryptoCurrencyStatus.currency.symbol, + ), + ) + } + } + + private suspend fun hasSignedHashes( + selectedWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus?, + ): Boolean { + return cryptoCurrencyStatus?.currency?.network?.let { + hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = it) + .conflate() + .distinctUntilChanged() + .firstOrNull() + } ?: false + } + + private fun MutableList.addRateTheAppNotification( + isReadyToShowRating: Boolean, + clickIntents: WalletClickIntentsV2, + ) { + addIf( + element = WalletNotification.RateApp( + onLikeClick = clickIntents::onLikeAppClick, + onDislikeClick = clickIntents::onDislikeAppClick, + onCloseClick = clickIntents::onCloseRateAppWarningClick, + ), + condition = isReadyToShowRating && readyForRateAppNotification, + ) + } + + private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { + if (condition) { + add(element = element) + if (element is WalletNotification.Critical || element is WalletNotification.Warning) { + readyForRateAppNotification = false + } + } + } + + private companion object { + const val MAX_REMAINING_SIGNATURES_COUNT = 10 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt new file mode 100644 index 0000000000..3426a96ae1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import arrow.core.Either +import com.tangem.common.extensions.isZero +import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import javax.inject.Inject + +internal class WalletWithFundsChecker @Inject constructor( + private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, +) { + + suspend fun check(maybeTokenList: Either) { + val tokenList = (maybeTokenList as? Either.Right)?.value ?: return + + val hasNonZeroWallets = when (tokenList) { + is TokenList.GroupedByNetwork -> { + tokenList.groups + .flatMap(NetworkGroup::currencies) + .hasNonZeroWallets() + } + is TokenList.Ungrouped -> tokenList.currencies.hasNonZeroWallets() + is TokenList.Empty -> false + } + + if (hasNonZeroWallets) setWalletWithFundsFoundUseCase() + } + + private fun List.hasNonZeroWallets(): Boolean { + return any { + val amount = it.value.amount ?: return@any false + !amount.isZero() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt new file mode 100644 index 0000000000..08b89be0c7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt @@ -0,0 +1,37 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetWarningsTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import kotlin.coroutines.CoroutineContext + +internal class MultiWalletWarningsSubscriber( + private val userWalletId: UserWalletId, + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) : WalletSubscriber>(name = "multi_wallet_warnings") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getMultiWalletWarningsFactory.create(clickIntents) + .conflate() + .distinctUntilChanged() + .onEach { + stateHolder.update( + SetWarningsTransformer(userWalletId = userWalletId, warnings = it), + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt new file mode 100644 index 0000000000..fde79c3d02 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt @@ -0,0 +1,92 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import arrow.core.Either +import com.tangem.common.extensions.isZero +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetPrimaryCurrencyTransformer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import java.math.BigDecimal +import kotlin.coroutines.CoroutineContext + +internal class PrimaryCurrencySubscriber( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val stateHolder: WalletStateHolderV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, +) : WalletSubscriber>(name = "primary_currency") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getPrimaryCurrencyStatusUpdatesUseCase(userWallet.walletId) + .conflate() + .distinctUntilChanged() + .onEach(::updateContent) + .onEach(::sendAnalyticsEvent) + .onEach(::checkWalletWithFunds) + } + + private fun updateContent(maybeCurrencyStatus: Either) { + val status = (maybeCurrencyStatus as? Either.Right)?.value ?: return + + stateHolder.update( + SetPrimaryCurrencyTransformer( + status = status.value, + userWallet = userWallet, + appCurrency = appCurrency, + ), + ) + } + + private fun sendAnalyticsEvent(maybeCurrencyStatus: Either) { + val status = (maybeCurrencyStatus as? Either.Right)?.value ?: return + + val fiatAmount = status.value.fiatAmount + val cardBalanceState = when (status.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.NoAmount, + -> createCardBalanceState(fiatAmount) + is CryptoCurrencyStatus.NoQuote -> AnalyticsParam.CardBalanceState.NoRate + is CryptoCurrencyStatus.Unreachable -> AnalyticsParam.CardBalanceState.BlockchainError + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Loading, + is CryptoCurrencyStatus.Custom, + -> null + } + + cardBalanceState?.let { + analyticsEventHandler.send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balance = it)) + } + } + + private fun createCardBalanceState(fiatAmount: BigDecimal?): AnalyticsParam.CardBalanceState? { + return when { + fiatAmount == null -> null + fiatAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty + else -> AnalyticsParam.CardBalanceState.Full + } + } + + private suspend fun checkWalletWithFunds(maybeCurrencyStatus: Either) { + val status = (maybeCurrencyStatus as? Either.Right)?.value ?: return + + if (status.value.amount?.isZero() == false) setWalletWithFundsFoundUseCase() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt new file mode 100644 index 0000000000..1f0de740d0 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt @@ -0,0 +1,45 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetCryptoCurrencyActionsTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import kotlin.coroutines.CoroutineContext + +internal class SingleWalletButtonsSubscriber( + private val userWallet: UserWallet, + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, +) : WalletSubscriber(name = "single_wallet_buttons") { + + override fun create(coroutineScope: CoroutineScope, uiDispatcher: CoroutineContext): Flow { + return channelFlow { + getPrimaryCurrencyStatusUpdatesUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> + getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = status) + .conflate() + .distinctUntilChanged() + .firstOrNull() + ?.let { send(it) } + } + } + .onEach(::updateContent) + } + + private fun updateContent(tokenActionsState: TokenActionsState) { + stateHolder.update( + SetCryptoCurrencyActionsTransformer( + tokenActionsState = tokenActionsState, + userWallet = userWallet, + clickIntents = clickIntents, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt new file mode 100644 index 0000000000..64df0d025e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt @@ -0,0 +1,40 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetWarningsTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import kotlin.coroutines.CoroutineContext + +/** +[REDACTED_AUTHOR] + */ +internal class SingleWalletNotificationsSubscriber( + private val userWalletId: UserWalletId, + private val stateHolder: WalletStateHolderV2, + private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, + private val clickIntents: WalletClickIntentsV2, +) : WalletSubscriber>(name = "single_wallet_warnings") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getSingleWalletWarningsFactory.create(clickIntents) + .conflate() + .distinctUntilChanged() + .onEach { + stateHolder.update( + SetWarningsTransformer(userWalletId = userWalletId, warnings = it), + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt new file mode 100644 index 0000000000..a6446944c0 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import arrow.core.Either +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListErrorTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import kotlin.coroutines.CoroutineContext + +@Suppress("LongParameterList") +internal class SingleWalletWithTokenListSubscriber( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getCardTokensListUseCase: GetCardTokensListUseCase, +) : WalletSubscriber>(name = "single_wallet_with_token_list") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getCardTokensListUseCase(userWalletId = userWallet.walletId) + .conflate() + .distinctUntilChanged() + .onEach(::updateContent) + .onEach(tokenListAnalyticsSender::send) + .onEach(walletWithFundsChecker::check) + } + + private fun updateContent(maybeTokenList: Either) { + stateHolder.update( + maybeTokenList.fold( + ifLeft = { SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it) }, + ifRight = { + SetTokenListTransformer( + tokenList = it, + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt new file mode 100644 index 0000000000..4c4caf4479 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import arrow.core.Either +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListErrorTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach +import kotlin.coroutines.CoroutineContext + +typealias MaybeTokenListFlow = Flow> + +@Suppress("LongParameterList") +internal class TokenListSubscriber( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getTokenListUseCase: GetTokenListUseCase, +) : WalletSubscriber>(name = "token_list") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return getTokenListUseCase(userWalletId = userWallet.walletId) + .conflate() + .distinctUntilChanged() + .onEach(::updateContent) + .onEach(tokenListAnalyticsSender::send) + .onEach(walletWithFundsChecker::check) + } + + private fun updateContent(maybeTokenList: Either) { + stateHolder.update( + maybeTokenList.fold( + ifLeft = { SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it) }, + ifRight = { + SetTokenListTransformer( + tokenList = it, + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt new file mode 100644 index 0000000000..6da06e3029 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -0,0 +1,108 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import androidx.paging.PagingData +import androidx.paging.cachedIn +import arrow.core.Either +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryCountErrorTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryCountTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryItemsErrorTransformer +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTxHistoryItemsTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlin.coroutines.CoroutineContext + +typealias MaybeTxHistoryCount = Either +typealias MaybeTxHistoryItems = Either>> + +@Suppress("LongParameterList") +internal class TxHistorySubscriber( + private val userWallet: UserWallet, + private val isRefresh: Boolean, + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, +) : WalletSubscriber>(name = "tx_history") { + + override fun create( + coroutineScope: CoroutineScope, + uiDispatcher: CoroutineContext, + ): Flow> { + return flow { + getPrimaryCurrencyStatusUpdatesUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> + val maybeTxHistoryItemCount = txHistoryItemsCountUseCase( + userWalletId = userWallet.walletId, + currency = status.currency, + ) + + setLoadingTxHistoryState(maybeTxHistoryItemCount, status) + + maybeTxHistoryItemCount.onRight { + val maybeTxHistoryItems = txHistoryItemsUseCase( + userWalletId = userWallet.walletId, + currency = status.currency, + refresh = isRefresh, + ).map { it.cachedIn(coroutineScope) } + + setLoadedTxHistoryState(maybeTxHistoryItems) + } + } + } + } + + private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { + stateHolder.update( + maybeTxHistoryItemCount.fold( + ifLeft = { + SetTxHistoryCountErrorTransformer( + userWallet = userWallet, + error = it, + pendingTransactions = status.value.pendingTransactions, + clickIntents = clickIntents, + ) + }, + ifRight = { + SetTxHistoryCountTransformer( + userWalletId = userWallet.walletId, + transactionsCount = it, + clickIntents = clickIntents, + ) + }, + ), + ) + } + + private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems) { + stateHolder.update( + maybeTxHistoryItems.fold( + ifLeft = { + SetTxHistoryItemsErrorTransformer( + userWalletId = userWallet.walletId, + error = it, + clickIntents = clickIntents, + ) + }, + ifRight = { + SetTxHistoryItemsTransformer( + userWallet = userWallet, + flow = it, + clickIntents = clickIntents, + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt new file mode 100644 index 0000000000..112c7cbe8a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt @@ -0,0 +1,30 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import timber.log.Timber +import kotlin.coroutines.CoroutineContext + +/** + * Component for implementation of flow subscription + * + * @property name unique name of subscriber + * [T] - type of flow + * +[REDACTED_AUTHOR] + */ +internal abstract class WalletSubscriber(val name: String) { + + protected abstract fun create(coroutineScope: CoroutineScope, uiDispatcher: CoroutineContext): Flow + + fun subscribe(coroutineScope: CoroutineScope, dispatchers: CoroutineDispatcherProvider): Job { + Timber.d("Subscribe on $name") + return create(coroutineScope, dispatchers.main) + .flowOn(dispatchers.main) + .launchIn(coroutineScope) + } +} \ No newline at end of file From f89ba800e985e59e4cb8acc4bc501c75c7a15ac8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 15:49:34 +0800 Subject: [PATCH 068/139] Updated on 2026-08-14 --- .../loaders/WalletContentLoaderFactory.kt | 40 ++++++++ .../wallet/loaders/WalletLoaderStorage.kt | 26 ++++++ .../loaders/WalletScreenContentLoader.kt | 91 +++++++++++++++++++ .../implementors/MultiWalletContentLoader.kt | 46 ++++++++++ .../MultiWalletContentLoaderFactory.kt | 39 ++++++++ .../implementors/SingleWalletContentLoader.kt | 66 ++++++++++++++ .../SingleWalletContentLoaderFactory.kt | 51 +++++++++++ .../SingleWalletWithTokenContentLoader.kt | 46 ++++++++++ ...ngleWalletWithTokenContentLoaderFactory.kt | 37 ++++++++ .../implementors/WalletContentLoader.kt | 19 ++++ .../intents/WalletCardClickIntents.kt | 5 +- .../intents/WalletClickIntentsV2.kt | 39 ++++---- .../intents/WalletContentClickIntents.kt | 2 + .../WalletCurrencyActionsClickIntents.kt | 2 + .../intents/WalletWarningsClickIntents.kt | 2 + 15 files changed, 489 insertions(+), 22 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/WalletContentLoader.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt new file mode 100644 index 0000000000..a6d0db40dc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt @@ -0,0 +1,40 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.MultiWalletContentLoaderFactory +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletContentLoaderFactory +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletWithTokenContentLoaderFactory +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.WalletContentLoader +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject + +@ViewModelScoped +internal class WalletContentLoaderFactory @Inject constructor( + private val multiWalletContentLoaderFactory: MultiWalletContentLoaderFactory, + private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoaderFactory, + private val singleWalletContentLoaderFactory: SingleWalletContentLoaderFactory, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + isRefresh: Boolean = false, + ): WalletContentLoader? { + return when { + userWallet.isMultiCurrency -> { + multiWalletContentLoaderFactory.create(userWallet, appCurrency, clickIntents) + } + userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() -> { + singleWalletWithTokenContentLoaderFactory.create(userWallet, appCurrency, clickIntents) + } + !userWallet.isMultiCurrency -> { + singleWalletContentLoaderFactory.create(userWallet, appCurrency, clickIntents, isRefresh) + } + else -> null + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt new file mode 100644 index 0000000000..11414466c8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders + +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.Job +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class WalletLoaderStorage @Inject constructor() { + + private val loaders = ConcurrentHashMap>() + + fun contains(id: UserWalletId) = loaders.containsKey(id) + + fun set(id: UserWalletId, jobs: List) { + loaders[id] = jobs + } + + fun remove(id: UserWalletId) { + loaders[id]?.let { + it.forEach(Job::cancel) + loaders.remove(id) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt new file mode 100644 index 0000000000..24b6e550ed --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt @@ -0,0 +1,91 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.CoroutineScope +import timber.log.Timber +import javax.inject.Inject + +/** + * Base wallet screen content loader. Use it to load content by [UserWallet]. + * + * @property factory factory that creates loader + * @property storage storage that save loader's jobs + * @property dispatchers coroutine dispatchers provider + * +[REDACTED_AUTHOR] + */ +@ViewModelScoped +internal class WalletScreenContentLoader @Inject constructor( + private val factory: WalletContentLoaderFactory, + private val storage: WalletLoaderStorage, + private val dispatchers: CoroutineDispatcherProvider, +) { + + /** + * Load content by [UserWallet] + * + * @param userWallet user wallet + * @param appCurrency app currency + * @param clickIntents click intents + * @param isRefresh flag that determinate if content must load again + * @param coroutineScope coroutine scope + */ + fun load( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + isRefresh: Boolean = false, + coroutineScope: CoroutineScope, + ) { + if (userWallet.isLocked) return + + val id = userWallet.walletId + if (!storage.contains(id)) { + loadInternal(userWallet, appCurrency, clickIntents, coroutineScope, isRefresh) + } else { + if (isRefresh) { + storage.remove(id) + loadInternal(userWallet, appCurrency, clickIntents, coroutineScope, true) + } else { + Timber.d("$id content loading has already started") + } + } + } + + /** Cancel loading by [id] */ + fun cancel(id: UserWalletId) { + Timber.d("$id content loading is canceled") + storage.remove(id) + } + + private fun loadInternal( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + coroutineScope: CoroutineScope, + isRefresh: Boolean, + ) { + val loader = factory.create( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + isRefresh = isRefresh, + ) + + if (loader == null) { + Timber.e("Impossible to create loader for $userWallet") + return + } + + Timber.d("${userWallet.walletId} content loading is ${if (isRefresh) "re" else ""}started") + + loader.subscribers + .map { it.subscribe(coroutineScope, dispatchers) } + .let { storage.set(userWallet.walletId, it) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt new file mode 100644 index 0000000000..6424208a3f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.TokenListSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 + +@Suppress("LongParameterList") +internal class MultiWalletContentLoader( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, + private val stateHolder: WalletStateHolderV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getTokenListUseCase: GetTokenListUseCase, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) : WalletContentLoader(id = userWallet.walletId) { + + override fun create(): List> { + return listOf( + TokenListSubscriber( + userWallet = userWallet, + appCurrency = appCurrency, + stateHolder = stateHolder, + clickIntents = clickIntents, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getTokenListUseCase = getTokenListUseCase, + ), + MultiWalletWarningsSubscriber( + userWalletId = userWallet.walletId, + stateHolder = stateHolder, + clickIntents = clickIntents, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt new file mode 100644 index 0000000000..1751d80a8b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -0,0 +1,39 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject + +@ViewModelScoped +internal class MultiWalletContentLoaderFactory @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val getTokenListUseCase: GetTokenListUseCase, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + ): WalletContentLoader { + return MultiWalletContentLoader( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + stateHolder = stateHolder, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getTokenListUseCase = getTokenListUseCase, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt new file mode 100644 index 0000000000..792c45ee35 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -0,0 +1,66 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.subscribers.* +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 + +@Suppress("LongParameterList") +internal class SingleWalletContentLoader( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, + private val isRefresh: Boolean, + private val stateHolder: WalletStateHolderV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, + private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, +) : WalletContentLoader(id = userWallet.walletId) { + + override fun create(): List> { + return listOf( + PrimaryCurrencySubscriber( + userWallet = userWallet, + appCurrency = appCurrency, + stateHolder = stateHolder, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, + analyticsEventHandler = analyticsEventHandler, + ), + SingleWalletButtonsSubscriber( + userWallet = userWallet, + stateHolder = stateHolder, + clickIntents = clickIntents, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, + ), + SingleWalletNotificationsSubscriber( + userWalletId = userWallet.walletId, + stateHolder = stateHolder, + clickIntents = clickIntents, + getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, + ), + TxHistorySubscriber( + userWallet = userWallet, + isRefresh = isRefresh, + stateHolder = stateHolder, + clickIntents = clickIntents, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, + txHistoryItemsUseCase = txHistoryItemsUseCase, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt new file mode 100644 index 0000000000..05eb03ee25 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt @@ -0,0 +1,51 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject + +@ViewModelScoped +@Suppress("LongParameterList") +internal class SingleWalletContentLoaderFactory @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, + private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + isRefresh: Boolean, + ): WalletContentLoader { + return SingleWalletContentLoader( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + isRefresh = isRefresh, + stateHolder = stateHolder, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, + getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, + setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, + txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, + txHistoryItemsUseCase = txHistoryItemsUseCase, + analyticsEventHandler = analyticsEventHandler, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt new file mode 100644 index 0000000000..fd3242278a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenListSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 + +@Suppress("LongParameterList") +internal class SingleWalletWithTokenContentLoader( + private val userWallet: UserWallet, + private val appCurrency: AppCurrency, + private val clickIntents: WalletClickIntentsV2, + private val stateHolder: WalletStateHolderV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getCardTokensListUseCase: GetCardTokensListUseCase, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) : WalletContentLoader(id = userWallet.walletId) { + + override fun create(): List> { + return listOf( + SingleWalletWithTokenListSubscriber( + userWallet = userWallet, + appCurrency = appCurrency, + stateHolder = stateHolder, + clickIntents = clickIntents, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getCardTokensListUseCase = getCardTokensListUseCase, + ), + MultiWalletWarningsSubscriber( + userWalletId = userWallet.walletId, + stateHolder = stateHolder, + clickIntents = clickIntents, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt new file mode 100644 index 0000000000..efef9eeb8d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -0,0 +1,37 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCardTokensListUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import javax.inject.Inject + +internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val walletWithFundsChecker: WalletWithFundsChecker, + private val getCardTokensListUseCase: GetCardTokensListUseCase, + private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, +) { + + fun create( + userWallet: UserWallet, + appCurrency: AppCurrency, + clickIntents: WalletClickIntentsV2, + ): SingleWalletWithTokenContentLoader { + return SingleWalletWithTokenContentLoader( + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + stateHolder = stateHolder, + tokenListAnalyticsSender = tokenListAnalyticsSender, + walletWithFundsChecker = walletWithFundsChecker, + getCardTokensListUseCase = getCardTokensListUseCase, + getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/WalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/WalletContentLoader.kt new file mode 100644 index 0000000000..83f9c28342 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/WalletContentLoader.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber + +/** + * Wallet content loader + * + * @property id loader id + * +[REDACTED_AUTHOR] + */ +internal abstract class WalletContentLoader(val id: UserWalletId) { + + /** Loader's subscribers */ + val subscribers: List> get() = create() + + protected abstract fun create(): List> +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index e7cc3d17a9..1bcff88b81 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -4,6 +4,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.UpdateWalletUseCase +import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState @@ -27,7 +28,7 @@ internal interface WalletCardClickIntents { internal class WalletCardClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateHolderV2, private val walletEventSender: WalletEventSender, - // TODO: private val walletScreenContentLoader: WalletScreenContentLoader, + private val walletScreenContentLoader: WalletScreenContentLoader, private val updateWalletUseCase: UpdateWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, private val dispatchers: CoroutineDispatcherProvider, @@ -51,7 +52,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { viewModelScope.launch(dispatchers.main) { - // TODO: walletScreenContentLoader.cancel(userWalletId) + walletScreenContentLoader.cancel(userWalletId) deleteWalletUseCase(userWalletId) .onRight { popBackIfAllWalletsIsLocked() } .onLeft { Timber.e(it.toString()) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt index 643c5dec03..55e0a76bc5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntentsV2.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase @@ -11,29 +12,29 @@ import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent import com.tangem.feature.wallet.presentation.wallet.domain.unwrap +import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state2.WalletState import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetRefreshStateTransformer import com.tangem.feature.wallet.presentation.wallet.state2.transformers.SetTokenListErrorTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject -/** -[REDACTED_AUTHOR] - */ @Suppress("LongParameterList") +@ViewModelScoped internal class WalletClickIntentsV2 @Inject constructor( private val walletCardClickIntentsImplementor: WalletCardClickIntentsImplementor, private val warningsClickIntentsImplementer: WalletWarningsClickIntentsImplementer, private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, private val stateHolder: WalletStateHolderV2, - // TODO: private val walletScreenContentLoader: WalletScreenContentLoader, + private val walletScreenContentLoader: WalletScreenContentLoader, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val selectWalletUseCase: SelectWalletUseCase, - // TODO: private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val fetchTokenListUseCase: FetchTokenListUseCase, private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, @@ -66,13 +67,12 @@ internal class WalletClickIntentsV2 @Inject constructor( stateHolder.update { it.copy(selectedWalletIndex = index) } maybeUserWallet.onRight { - // TODO: - // walletScreenContentLoader.load( - // userWallet = it, - // appCurrency = getSelectedAppCurrencyUseCase.unwrap(), - // clickIntents = this@WalletClickIntentsV2, - // coroutineScope = viewModelScope, - // ) + walletScreenContentLoader.load( + userWallet = it, + appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + clickIntents = this@WalletClickIntentsV2, + coroutineScope = viewModelScope, + ) } } } @@ -133,14 +133,13 @@ internal class WalletClickIntentsV2 @Inject constructor( viewModelScope.launch(dispatchers.main) { fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) - // TODO: - // walletScreenContentLoader.load( - // userWallet = userWallet, - // appCurrency = getSelectedAppCurrencyUseCase.unwrap(), - // clickIntents = this@WalletClickIntentsV2, - // coroutineScope = viewModelScope, - // isRefresh = true, - // ) + walletScreenContentLoader.load( + userWallet = userWallet, + appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + clickIntents = this@WalletClickIntentsV2, + coroutineScope = viewModelScope, + isRefresh = true, + ) stateHolder.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 82c943e41b..a3582c6e63 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -19,6 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBo import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state2.transformers.converter.MultiWalletCurrencyActionsConverter import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch @@ -42,6 +43,7 @@ internal interface WalletContentClickIntents { } @Suppress("LongParameterList") +@ViewModelScoped internal class WalletContentClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateHolderV2, private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 0b6070971a..9690fa7684 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -37,6 +37,7 @@ import com.tangem.feature.wallet.presentation.wallet.state2.transformers.CloseBo import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take @@ -65,6 +66,7 @@ interface WalletCurrencyActionsClickIntents { } @Suppress("LongParameterList", "LargeClass") +@ViewModelScoped internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateHolderV2, private val walletEventSender: WalletEventSender, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index b4b9b2d06f..cfa8be01ab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -38,6 +38,7 @@ import com.tangem.feature.wallet.presentation.wallet.state2.transformers.OpenBot import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.launch import javax.inject.Inject @@ -63,6 +64,7 @@ internal interface WalletWarningsClickIntents { } @Suppress("LongParameterList") +@ViewModelScoped internal class WalletWarningsClickIntentsImplementer @Inject constructor( private val stateHolder: WalletStateHolderV2, private val walletEventSender: WalletEventSender, From 236b977e46d06909d5a5bcf0e2982f978264f6d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 15:56:35 +0800 Subject: [PATCH 069/139] Updated on 2026-08-14 --- .../tokens/legacy/redux/TokensMiddleware.kt | 18 +- .../configs/feature_toggles_config.json | 4 + .../com/tangem/core/ui/event/EventEffect.kt | 5 +- .../featuretoggles/WalletFeatureToggles.kt | 2 + .../feature/wallet/di/WalletRouterModule.kt | 8 +- .../DefaultWalletFeatureToggles.kt | 3 + .../router/DefaultWalletRouter.kt | 23 +- .../wallet/viewmodels/WalletViewModel.kt | 9 +- .../wallet/viewmodels/WalletViewModelV2.kt | 311 ++++++++++++++++++ .../WalletsUpdateActionResolverV2.kt | 305 +++++++++++++++++ 10 files changed, 670 insertions(+), 18 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolverV2.kt diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt index 1ec868ce0e..101c7c51e5 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt @@ -20,6 +20,8 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.TokenWithBlockchain import com.tangem.domain.tokens.TokensAction import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* @@ -90,13 +92,17 @@ object TokensMiddleware { if (scanResponse.supportsHdWallet()) { deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) { submitNewAdd( - userWalletId = action.userWallet.walletId, + userWallet = action.userWallet, updatedScanResponse = it, currencyList = currencyList, ) } } else { - submitNewAdd(userWalletId = action.userWallet.walletId, scanResponse, currencyList = currencyList) + submitNewAdd( + userWallet = action.userWallet, + updatedScanResponse = scanResponse, + currencyList = currencyList, + ) } } } @@ -375,16 +381,18 @@ object TokensMiddleware { } private fun submitNewAdd( - userWalletId: UserWalletId, + userWallet: UserWallet, updatedScanResponse: ScanResponse, currencyList: List, ) { scope.launch { userWalletsListManager.update( - userWalletId = userWalletId, + userWalletId = userWallet.walletId, update = { it.copy(scanResponse = updatedScanResponse) }, ).doOnSuccess { - addCryptoCurrenciesUseCase(userWalletId, currencyList) + addCryptoCurrenciesUseCase(userWallet.walletId, currencyList).onRight { + store.dispatch(action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet)) + } } } store.dispatchOnMain(NavigationAction.PopBackTo()) diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 9da64e96a0..106d635c23 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -34,5 +34,9 @@ { "name": "REDESIGNED_SEND_SCREEN_ENABLED", "version": "undefined" + }, + { + "name": "WALLETS_SCROLLING_PREVIEW_ENABLED", + "version": "5.4.0" } ] diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt index e6f3ad0d84..516dba38e6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.event import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.NonRestartableComposable +import kotlinx.coroutines.launch /** * A Composable function that reacts to a given [StateEvent], executing the provided action only once when the event @@ -17,8 +18,8 @@ import androidx.compose.runtime.NonRestartableComposable fun EventEffect(event: StateEvent, onTrigger: suspend (data: A) -> Unit) { LaunchedEffect(event) { if (event is StateEvent.Triggered) { - onTrigger(event.data) - event.onConsume() + launch { onTrigger(event.data) } + .invokeOnCompletion { event.onConsume() } } } } \ No newline at end of file diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt index 5af5379a9e..9485b4d30c 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt @@ -9,4 +9,6 @@ interface WalletFeatureToggles { /** Availability of redesigned screen */ val isRedesignedScreenEnabled: Boolean + + val isWalletsScrollingPreviewEnabled: Boolean } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt index 1e64fc4122..81c4fd83ef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.di import com.tangem.core.navigation.ReduxNavController import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.features.wallet.navigation.WalletRouter import dagger.Module import dagger.Provides @@ -15,7 +16,10 @@ internal object WalletRouterModule { @Provides @ActivityScoped - fun provideWalletRouter(reduxNavController: ReduxNavController): WalletRouter { - return DefaultWalletRouter(reduxNavController = reduxNavController) + fun provideWalletRouter( + reduxNavController: ReduxNavController, + walletFeatureToggles: WalletFeatureToggles, + ): WalletRouter { + return DefaultWalletRouter(reduxNavController = reduxNavController, walletFeatureToggles = walletFeatureToggles) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt index 71696ceba1..09655bdd61 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt @@ -16,4 +16,7 @@ internal class DefaultWalletFeatureToggles( override val isRedesignedScreenEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_WALLET_SCREEN_ENABLED") + + override val isWalletsScrollingPreviewEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "WALLETS_SCROLLING_PREVIEW_ENABLED") } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 646cec18f5..5dd19e4a33 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -26,12 +26,18 @@ import com.tangem.feature.wallet.presentation.WalletFragment import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen +import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreenV2 import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModelV2 import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlin.properties.Delegates /** Default implementation of wallet feature router */ -internal class DefaultWalletRouter(private val reduxNavController: ReduxNavController) : InnerWalletRouter { +internal class DefaultWalletRouter( + private val reduxNavController: ReduxNavController, + private val walletFeatureToggles: WalletFeatureToggles, +) : InnerWalletRouter { private var navController: NavHostController by Delegates.notNull() private var onFinish: () -> Unit = {} @@ -47,10 +53,19 @@ internal class DefaultWalletRouter(private val reduxNavController: ReduxNavContr startDestination = WalletRoute.Wallet.route, ) { composable(WalletRoute.Wallet.route) { - val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } - LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + if (walletFeatureToggles.isWalletsScrollingPreviewEnabled) { + val viewModel = hiltViewModel().apply { + router = this@DefaultWalletRouter + } - WalletScreen(state = viewModel.uiState) + WalletScreen(state = viewModel.uiState) + } else { + val viewModel = hiltViewModel().apply { + setWalletRouter(router = this@DefaultWalletRouter) + } + + WalletScreenV2(state = viewModel.uiState.collectAsStateWithLifecycle().value) + } } composable( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index bc1c275b2f..1cb74caecf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -68,6 +68,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.factory.TokenListWithWallet import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletStateFactory +import com.tangem.feature.wallet.presentation.wallet.subscribers.MaybeTokenListFlow import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -129,10 +130,10 @@ internal class WalletViewModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val scanCardToUnlockWalletUseCase: ScanCardToUnlockWalletClickHandler, - hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, isNeedToBackupUseCase: IsNeedToBackupUseCase, getMissedAddressesCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase, + hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, // endregion Parameters ) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { @@ -1352,7 +1353,7 @@ internal class WalletViewModel @Inject constructor( } } - private suspend fun updateButtons(userWallet: UserWallet, currencyStatus: CryptoCurrencyStatus) { + private fun updateButtons(userWallet: UserWallet, currencyStatus: CryptoCurrencyStatus) { getCryptoCurrencyActionsUseCase( userWallet = userWallet, cryptoCurrencyStatus = currencyStatus, @@ -1416,6 +1417,4 @@ internal class WalletViewModel @Inject constructor( } private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver -} - -typealias MaybeTokenListFlow = Flow> \ No newline at end of file +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt new file mode 100644 index 0000000000..a9a2908965 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModelV2.kt @@ -0,0 +1,311 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse +import com.tangem.common.Provider +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.CanUseBiometryUseCase +import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled +import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase +import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent.DemonstrateWalletsScrollPreview.Direction +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletStateHolderV2 +import com.tangem.feature.wallet.presentation.wallet.state2.transformers.* +import com.tangem.feature.wallet.presentation.wallet.state2.utils.WalletEventSender +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntentsV2 +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject +import kotlin.properties.Delegates + +@Suppress("LongParameterList") +@HiltViewModel +internal class WalletViewModelV2 @Inject constructor( + private val stateHolder: WalletStateHolderV2, + private val clickIntents: WalletClickIntentsV2, + private val walletEventSender: WalletEventSender, + private val walletsUpdateActionResolver: WalletsUpdateActionResolverV2, + private val walletScreenContentLoader: WalletScreenContentLoader, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase, + private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + analyticsEventsHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, + private val reduxStateHolder: ReduxStateHolder, +) : ViewModel() { + + val uiState: StateFlow = stateHolder.uiState + + private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() + + private var router: InnerWalletRouter by Delegates.notNull() + private var walletsUpdateJobHolder: JobHolder = JobHolder() + + init { + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) + + suggestToEnableBiometrics() + + subscribeOnWalletsUpdateFlow() + subscribeOnBalanceHiding() + subscribeOnSelectedWalletFlow() + } + + fun setWalletRouter(router: InnerWalletRouter) { + this.router = router + clickIntents.initialize(router, viewModelScope) + } + + private fun suggestToEnableBiometrics() { + viewModelScope.launch(dispatchers.main) { + withContext(dispatchers.io) { delay(timeMillis = 1_800) } + + if (isShowSaveWalletScreenEnabled()) router.openSaveUserWalletScreen() + } + } + + private suspend fun isShowSaveWalletScreenEnabled(): Boolean { + return router.isWalletLastScreen() && shouldShowSaveWalletScreenUseCase() && canUseBiometryUseCase() + } + + private fun subscribeOnWalletsUpdateFlow() { + viewModelScope.launch(dispatchers.main) { + shouldSaveUserWalletsUseCase() + .conflate() + .distinctUntilChanged() + .collectLatest { shouldSaveUserWallet -> + getWalletsUseCase() + .distinctUntilChanged() + .conflate() + .map { + walletsUpdateActionResolver.resolve( + wallets = it, + currentState = stateHolder.value, + canSaveWallets = shouldSaveUserWallet, + ) + } + .onEach(::updateWallets) + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + .saveIn(walletsUpdateJobHolder) + } + } + } + + private fun subscribeOnBalanceHiding() { + getBalanceHidingSettingsUseCase() + .conflate() + .distinctUntilChanged() + .onEach { + stateHolder.update(transformer = UpdateBalanceHidingModeTransformer(it.isBalanceHidden)) + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } + + private fun subscribeOnSelectedWalletFlow() { + getSelectedWalletUseCase().onRight { + it + .conflate() + .distinctUntilChanged() + .onEach { selectedWallet -> + if (selectedWallet.isMultiCurrency) { + reduxStateHolder.dispatch( + action = WalletConnectActions.New.Initialize(userWallet = selectedWallet), + ) + } + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } + } + + private fun updateWallets(action: WalletsUpdateActionResolverV2.Action) { + when (action) { + is WalletsUpdateActionResolverV2.Action.InitializeWallets -> initializeWallets(action) + is WalletsUpdateActionResolverV2.Action.ReinitializeWallets -> { + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + isRefresh = true, + ) + } + is WalletsUpdateActionResolverV2.Action.ReinitializeWallet -> reinitializeWallet(action) + is WalletsUpdateActionResolverV2.Action.AddWallet -> addWallet(action) + is WalletsUpdateActionResolverV2.Action.DeleteWallet -> deleteWallet(action) + is WalletsUpdateActionResolverV2.Action.UnlockWallet -> unlockWallet(action) + is WalletsUpdateActionResolverV2.Action.UpdateWalletCardCount -> { + stateHolder.update(transformer = UpdateWalletCardsCountTransformer(action.selectedWallet)) + } + is WalletsUpdateActionResolverV2.Action.UpdateWalletName -> { + stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name)) + } + is WalletsUpdateActionResolverV2.Action.Unknown -> Unit + } + } + + private fun initializeWallets(action: WalletsUpdateActionResolverV2.Action.InitializeWallets) { + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + stateHolder.update( + transformer = InitializeWalletsTransformer( + selectedWalletIndex = action.selectedWalletIndex, + selectedWallet = action.selectedWallet, + wallets = action.wallets, + clickIntents = clickIntents, + ), + ) + + viewModelScope.launch(dispatchers.main) { + if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) { + withContext(dispatchers.io) { + delay(timeMillis = 1_800) + } + + walletEventSender.send( + event = WalletEvent.DemonstrateWalletsScrollPreview( + direction = if (action.selectedWalletIndex == action.wallets.lastIndex) { + Direction.RIGHT + } else { + Direction.LEFT + }, + ), + ) + } + } + } + + private fun reinitializeWallet(action: WalletsUpdateActionResolverV2.Action.ReinitializeWallet) { + viewModelScope.launch(dispatchers.main) { + walletScreenContentLoader.cancel(action.prevWalletId) + + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + stateHolder.update( + ReinitializeWalletTransformer(userWallet = action.selectedWallet, clickIntents = clickIntents), + ) + } + } + + private fun addWallet(action: WalletsUpdateActionResolverV2.Action.AddWallet) { + viewModelScope.launch(dispatchers.main) { + stateHolder.update( + AddWalletTransformer( + userWallet = action.selectedWallet, + clickIntents = clickIntents, + ), + ) + + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + withContext(dispatchers.io) { delay(timeMillis = 700) } + + scrollToWallet(index = action.selectedWalletIndex) + } + } + + private fun deleteWallet(action: WalletsUpdateActionResolverV2.Action.DeleteWallet) { + viewModelScope.launch(dispatchers.main) { + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + + scrollToWallet(index = action.selectedWalletIndex) + + withContext(dispatchers.io) { delay(timeMillis = 700) } + + stateHolder.update( + DeleteWalletTransformer( + selectedWalletIndex = action.selectedWalletIndex, + deletedWalletId = action.deletedWalletId, + ), + ) + } + } + + private fun unlockWallet(action: WalletsUpdateActionResolverV2.Action.UnlockWallet) { + viewModelScope.launch(dispatchers.main) { + withContext(dispatchers.io) { delay(timeMillis = 700) } + + stateHolder.update( + transformer = UnlockWalletTransformer( + unlockedWallets = action.unlockedWallets, + clickIntents = clickIntents, + ), + ) + + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + appCurrency = selectedAppCurrencyFlow.value, + clickIntents = clickIntents, + coroutineScope = viewModelScope, + ) + } + } + + private fun scrollToWallet(index: Int) { + stateHolder.update( + ScrollToWalletTransformer( + index = index, + currentStateProvider = Provider(action = stateHolder::value), + stateUpdater = { newState -> stateHolder.update { newState } }, + ), + ) + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolverV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolverV2.kt new file mode 100644 index 0000000000..821148d80c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolverV2.kt @@ -0,0 +1,305 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state2.NOT_INITIALIZED_WALLET_INDEX +import com.tangem.feature.wallet.presentation.wallet.state2.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state2.WalletState +import dagger.hilt.android.scopes.ViewModelScoped +import timber.log.Timber +import javax.inject.Inject + +/** + * Resolver that determines which update action will be performed + * + * @property getSelectedWalletSyncUseCase use case that returns selected wallet + */ +@ViewModelScoped +internal class WalletsUpdateActionResolverV2 @Inject constructor( + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, +) { + + private var isInitialized: Boolean = false + private var canSaveWallets: Boolean = false + + fun resolve(wallets: List, currentState: WalletScreenState, canSaveWallets: Boolean): Action { + val selectedWallet = wallets.getSelectedWallet() ?: return Action.Unknown + + val action = when { + isFirstInitialization(currentState) -> { + createInitializeWalletsAction(wallets, selectedWallet, canSaveWallets) + } + isReinitialization(canSaveWallets) -> { + this.canSaveWallets = canSaveWallets + Action.ReinitializeWallets(selectedWallet = selectedWallet) + } + else -> getUpdateContentAction(currentState, wallets, selectedWallet) + } + + Timber.d("Resolved action: $action") + + return action + } + + private fun List.getSelectedWallet(): UserWallet? { + return when { + isEmpty() -> null + size == 1 -> first() + else -> getSelectedWalletSyncUseCase().fold(ifLeft = { null }, ifRight = { it }) + } + } + + private fun isFirstInitialization(state: WalletScreenState): Boolean { + return state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX + } + + private fun createInitializeWalletsAction( + wallets: List, + selectedWallet: UserWallet, + canSaveWallets: Boolean, + ): Action { + this.isInitialized = true + this.canSaveWallets = canSaveWallets + + return Action.InitializeWallets( + selectedWalletIndex = wallets.indexOfWallet(selectedWallet.walletId), + selectedWallet = selectedWallet, + wallets = wallets, + ) + } + + private fun isReinitialization(canSaveWallets: Boolean): Boolean { + return isInitialized && this.canSaveWallets != canSaveWallets + } + + private fun getUpdateContentAction( + state: WalletScreenState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + return when { + isWalletsCountChanged(state, wallets) -> { + getChangeWalletsListAction(state, wallets, selectedWallet) + } + isAnotherWalletSelected(state, selectedWallet) -> { + Action.ReinitializeWallet( + prevWalletId = state.getPrevSelectedWallet().id, + selectedWallet = selectedWallet, + ) + } + else -> getUpdateSelectedWalletAction(state, wallets, selectedWallet) + } + } + + private fun isWalletsCountChanged(state: WalletScreenState, wallets: List): Boolean { + val prevWalletsSize = state.wallets.size + val walletsSize = wallets.size + + return prevWalletsSize != walletsSize + } + + private fun getChangeWalletsListAction( + state: WalletScreenState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + val prevWalletsSize = state.wallets.size + + return when { + prevWalletsSize > wallets.size -> { + Action.DeleteWallet( + selectedWallet = selectedWallet, + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + deletedWalletId = state.wallets.getDeletedWalletId(wallets), + ) + } + prevWalletsSize < wallets.size -> { + val newUserWallet = state.wallets.getAddedWallet(wallets) + Action.AddWallet( + selectedWalletIndex = wallets.indexOfWallet(id = newUserWallet.walletId), + selectedWallet = newUserWallet, + ) + } + else -> error("Wallets list is not changed") + } + } + + private fun List.getDeletedWalletId(wallets: List): UserWalletId { + return this + .map { it.walletCardState.id } + .firstOrNull { !wallets.map(UserWallet::walletId).contains(it) } + ?: error("Deleted wallet id is not found. Wallets contains all previous wallets ids") + } + + private fun List.getAddedWallet(wallets: List): UserWallet { + return wallets + .firstOrNull { wallet -> !this.map { it.walletCardState.id }.contains(wallet.walletId) } + ?: error("Added wallet id is not found. Wallets contains all previous wallets ids") + } + + private fun isAnotherWalletSelected(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + return state.getPrevSelectedWallet().id != selectedWallet.walletId + } + + private fun getUpdateSelectedWalletAction( + state: WalletScreenState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + return when { + isSelectedWalletNameChanged(state, selectedWallet) -> { + Action.UpdateWalletName(selectedWalletId = selectedWallet.walletId, name = selectedWallet.name) + } + isSelectedWalletUnlocked(state, selectedWallet) -> { + Action.UnlockWallet( + selectedWallet = selectedWallet, + unlockedWallets = wallets.filterNot(UserWallet::isLocked), + ) + } + isSelectedWalletCardsCountChanged(state, selectedWallet) -> Action.UpdateWalletCardCount(selectedWallet) + else -> Action.Unknown + } + } + + private fun isSelectedWalletNameChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + return state.getPrevSelectedWallet().title != selectedWallet.name + } + + private fun isSelectedWalletUnlocked(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + return state.isSelectedWalletLocked() && !selectedWallet.isLocked + } + + private fun isSelectedWalletCardsCountChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean { + val prevSelectedWallet = state.getPrevSelectedWallet() + return prevSelectedWallet is WalletCardState.Content && + prevSelectedWallet.cardCount != selectedWallet.getCardsCount() + } + + private fun WalletScreenState.isSelectedWalletLocked(): Boolean { + val selectedWalletState = wallets.getOrNull(selectedWalletIndex) ?: error("Selected wallet is not found") + return selectedWalletState is WalletState.MultiCurrency.Locked || + selectedWalletState is WalletState.SingleCurrency.Locked + } + + private fun WalletScreenState.getPrevSelectedWallet(): WalletCardState { + return wallets + .map(WalletState::walletCardState) + .getOrNull(selectedWalletIndex) + ?: error("Previous selected wallet is not found") + } + + private fun List.indexOfWallet(id: UserWalletId): Int { + val selectedIndex = indexOfFirst { it.walletId == id } + + return if (selectedIndex == -1) { + error("Wallets don't contain a wallet with id: $id") + } else { + selectedIndex + } + } + + sealed class Action { + + data class InitializeWallets( + val selectedWalletIndex: Int, + val selectedWallet: UserWallet, + val wallets: List, + ) : Action() { + + override fun toString(): String { + return """ + Initialize( + selectedWalletIndex=$selectedWalletIndex, + selectedWallet=${selectedWallet.walletId}, + wallets=${wallets.joinToString { it.walletId.toString() }} + ) + """.trimIndent() + } + } + + /** + * Reinitialize wallets. Example, if user turned on wallets saving + * + * @property selectedWallet selected wallet + */ + data class ReinitializeWallets(val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "Reinitialize(selectedWallet=${selectedWallet.walletId})" + } + } + + /** + * Reinitialize selected wallet. Example, scanning a new card if wallets saving is turned off + * + * @property prevWalletId previous selected wallet id + * @property selectedWallet selected wallet + */ + data class ReinitializeWallet(val prevWalletId: UserWalletId, val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "ReinitializeWallet(prevWalletId=$prevWalletId, selectedWallet=${selectedWallet.walletId})" + } + } + + data class UpdateWalletName(val selectedWalletId: UserWalletId, val name: String) : Action() { + + override fun toString(): String { + return "UpdateWalletName(selectedWalletId=$selectedWalletId, name=$name)" + } + } + + data class UnlockWallet(val selectedWallet: UserWallet, val unlockedWallets: List) : Action() { + + override fun toString(): String { + return """ + UnlockWallet( + selectedWallet=${selectedWallet.walletId}, + unlockedWallets=${unlockedWallets.joinToString { it.walletId.toString() }} + ) + """.trimIndent() + } + } + + data class DeleteWallet( + val selectedWallet: UserWallet, + val selectedWalletIndex: Int, + val deletedWalletId: UserWalletId, + ) : Action() { + + override fun toString(): String { + return """ + DeleteWallet( + selectedWallet=${selectedWallet.walletId}, + selectedWalletIndex=$selectedWalletIndex, + deletedWalletId=$deletedWalletId + ) + """.trimIndent() + } + } + + data class AddWallet(val selectedWalletIndex: Int, val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "AddWallet(selectedWalletIndex=$selectedWalletIndex, selectedWallet=${selectedWallet.walletId})" + } + } + + /** + * Update wallet card count. Example, if user backed up wallet + * + * @property selectedWallet selected wallet + */ + data class UpdateWalletCardCount(val selectedWallet: UserWallet) : Action() { + + override fun toString(): String { + return "UpdateWalletCardCount(selectedWallet=${selectedWallet.walletId})" + } + } + + object Unknown : Action() + } +} \ No newline at end of file From 29bdad0743946234b22e23ac80e5b3fe61452f40 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 14:26:14 +0200 Subject: [PATCH 070/139] Updated on 2026-08-14 --- .../repository/DefaultCurrenciesRepository.kt | 1 - .../feature/swap/domain/SwapInteractor.kt | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 56 +++++++------------ .../swap/domain/di/SwapDomainModule.kt | 11 +--- .../feature/swap/viewmodels/SwapViewModel.kt | 1 - 5 files changed, 21 insertions(+), 49 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 8e5decb644..85e522335d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -381,7 +381,6 @@ internal class DefaultCurrenciesRepository( ) { try { val tokensList = userTokens.tokens - .distinctBy { it.networkId } .map { LeastTokenInfo( contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index aa76195519..6f550682f4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -4,7 +4,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 15bcdad9af..da5dd7b595 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,17 +1,13 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse -import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.SendTransactionError @@ -28,7 +24,6 @@ import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* @@ -49,9 +44,6 @@ internal class SwapInteractorImpl @Inject constructor( private val repository: SwapRepository, private val cache: SwapDataCache, private val allowPermissionsHandler: AllowPermissionsHandler, - private val currenciesRepository: CurrenciesRepository, - private val networksRepository: NetworksRepository, - private val walletFeatureToggles: WalletFeatureToggles, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val walletManagersFacade: WalletManagersFacade, @@ -60,11 +52,6 @@ internal class SwapInteractorImpl @Inject constructor( private val dispatcher: CoroutineDispatcherProvider, ) : SwapInteractor { - // TODO: Move to DI - private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) { - AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository) - } - private val getFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { GetFeeUseCase(walletManagersFacade, dispatcher) } @@ -384,7 +371,7 @@ internal class SwapInteractorImpl @Inject constructor( amount = amount, fee = fee, providerId = swapProvider.providerId, - userWalletId = requireNotNull(getSelectedWallet()).walletId + userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } ExchangeProviderType.DEX -> { @@ -485,7 +472,7 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, fee: TxFee, providerId: String, - userWalletId: UserWalletId + userWalletId: UserWalletId, ): TxState { val exchangeData = repository.getExchangeData( fromContractAddress = currencyToSend.currency.getContractAddress(), @@ -511,37 +498,32 @@ internal class SwapInteractorImpl @Inject constructor( val result = sendTransactionUseCase( requireNotNull(txData), userWallet = requireNotNull(getSelectedWallet()), - network = currencyToSend.currency.network + network = currencyToSend.currency.network, ) - return result.fold(ifLeft = { - when(it){ + when (it) { is SendTransactionError.NetworkError -> TxState.NetworkError is SendTransactionError.DataError -> TxState.BlockchainError SendTransactionError.DemoCardError -> TxState.UnknownError else -> TxState.UnknownError } - }, ifRight = { - TxState.TxSent( - fromAmount = amountFormatter.formatSwapAmountToUI( - amount, - currencyToSend.currency.symbol, - ), - toAmount = amountFormatter.formatSwapAmountToUI( - exchangeData.dataModel.toTokenAmount, - currencyToGet.currency.symbol, - ), - txAddress = userWalletManager.getLastTransactionHash(currencyToSend.currency.network.backendId, derivationPath) ?: "", - ) - TxState.TxSent( - txAddress = userWalletManager.getLastTransactionHash( - networkId = currencyToSend.currency.network.backendId, - derivationPath = derivationPath - ) ?: "" - ) - }) + TxState.TxSent( + fromAmount = amountFormatter.formatSwapAmountToUI( + amount, + currencyToSend.currency.symbol, + ), + toAmount = amountFormatter.formatSwapAmountToUI( + exchangeData.dataModel.toTokenAmount, + currencyToGet.currency.symbol, + ), + txAddress = userWalletManager.getLastTransactionHash( + currencyToSend.currency.network.backendId, + derivationPath, + ) ?: "", + ) + },) } @Deprecated("used in old swap mechanism") diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 9c06a31f85..734efb5f0d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -35,9 +35,6 @@ class SwapDomainModule { swapRepository: SwapRepository, userWalletManager: UserWalletManager, transactionManager: TransactionManager, - currenciesRepository: CurrenciesRepository, - networksRepository: NetworksRepository, - walletFeatureToggles: WalletFeatureToggles, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, @SwapScope sendTransactionUseCase: SendTransactionUseCase, @@ -51,9 +48,6 @@ class SwapDomainModule { repository = swapRepository, cache = SwapDataCacheImpl(), allowPermissionsHandler = AllowPermissionsHandlerImpl(), - currenciesRepository = currenciesRepository, - networksRepository = networksRepository, - walletFeatureToggles = walletFeatureToggles, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, sendTransactionUseCase = sendTransactionUseCase, @@ -114,7 +108,7 @@ class SwapDomainModule { @SwapScope @Provides - fun provideDemoCardUseCase() : IsDemoCardUseCase { + fun provideDemoCardUseCase(): IsDemoCardUseCase { return IsDemoCardUseCase(config = DemoConfig()) } @@ -129,10 +123,9 @@ class SwapDomainModule { return SendTransactionUseCase( isDemoCardUseCase = isDemoCardUseCase, cardSdkConfigRepository = cardSdkConfigRepository, - walletManagersFacade = walletManagersFacade + walletManagersFacade = walletManagersFacade, ) } - } @Qualifier diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index d3121006ab..b89d4e193d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -17,7 +17,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor -import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider From 7f71ef182eb09c1a920e27cefd39840abda59749 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 20:42:03 +0300 Subject: [PATCH 071/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractor.kt | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 50 ++++---- .../swap/domain/di/SwapDomainModule.kt | 5 +- .../feature/swap/models/SwapStateHolder.kt | 2 + .../tangem/feature/swap/models/UiActions.kt | 1 + .../swap/models/states/ProviderState.kt | 5 +- .../swap/ui/ChooseProviderBottomSheet.kt | 4 +- .../tangem/feature/swap/ui/ProviderItem.kt | 40 ++++-- .../tangem/feature/swap/ui/StateBuilder.kt | 119 ++++++++++++++++-- .../feature/swap/ui/SwapScreenContent.kt | 14 ++- .../feature/swap/viewmodels/SwapViewModel.kt | 80 ++++++++++-- 11 files changed, 260 insertions(+), 61 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index aa76195519..6f550682f4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -4,7 +4,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 15bcdad9af..3907116097 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse -import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase @@ -384,7 +383,7 @@ internal class SwapInteractorImpl @Inject constructor( amount = amount, fee = fee, providerId = swapProvider.providerId, - userWalletId = requireNotNull(getSelectedWallet()).walletId + userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } ExchangeProviderType.DEX -> { @@ -485,7 +484,7 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, fee: TxFee, providerId: String, - userWalletId: UserWalletId + userWalletId: UserWalletId, ): TxState { val exchangeData = repository.getExchangeData( fromContractAddress = currencyToSend.currency.getContractAddress(), @@ -511,37 +510,38 @@ internal class SwapInteractorImpl @Inject constructor( val result = sendTransactionUseCase( requireNotNull(txData), userWallet = requireNotNull(getSelectedWallet()), - network = currencyToSend.currency.network + network = currencyToSend.currency.network, ) - return result.fold(ifLeft = { - when(it){ + when (it) { is SendTransactionError.NetworkError -> TxState.NetworkError is SendTransactionError.DataError -> TxState.BlockchainError SendTransactionError.DemoCardError -> TxState.UnknownError else -> TxState.UnknownError } - }, ifRight = { - TxState.TxSent( - fromAmount = amountFormatter.formatSwapAmountToUI( - amount, - currencyToSend.currency.symbol, - ), - toAmount = amountFormatter.formatSwapAmountToUI( - exchangeData.dataModel.toTokenAmount, - currencyToGet.currency.symbol, - ), - txAddress = userWalletManager.getLastTransactionHash(currencyToSend.currency.network.backendId, derivationPath) ?: "", - ) - TxState.TxSent( - txAddress = userWalletManager.getLastTransactionHash( - networkId = currencyToSend.currency.network.backendId, - derivationPath = derivationPath - ) ?: "" - ) - }) + TxState.TxSent( + fromAmount = amountFormatter.formatSwapAmountToUI( + amount, + currencyToSend.currency.symbol, + ), + toAmount = amountFormatter.formatSwapAmountToUI( + exchangeData.dataModel.toTokenAmount, + currencyToGet.currency.symbol, + ), + txAddress = userWalletManager.getLastTransactionHash( + currencyToSend.currency.network.backendId, + derivationPath, + ) ?: "", + ) + TxState.TxSent( + txAddress = userWalletManager.getLastTransactionHash( + networkId = currencyToSend.currency.network.backendId, + derivationPath = derivationPath, + ) ?: "", + ) + },) } @Deprecated("used in old swap mechanism") diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 9c06a31f85..16fb6a7285 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -114,7 +114,7 @@ class SwapDomainModule { @SwapScope @Provides - fun provideDemoCardUseCase() : IsDemoCardUseCase { + fun provideDemoCardUseCase(): IsDemoCardUseCase { return IsDemoCardUseCase(config = DemoConfig()) } @@ -129,10 +129,9 @@ class SwapDomainModule { return SendTransactionUseCase( isDemoCardUseCase = isDemoCardUseCase, cardSdkConfigRepository = cardSdkConfigRepository, - walletManagersFacade = walletManagersFacade + walletManagersFacade = walletManagersFacade, ) } - } @Qualifier diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 61f1089d60..37ef409ce5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -121,6 +121,8 @@ sealed interface SwapWarning { * @property priceImpact in format = 10 (means 10%) */ data class HighPriceImpact(val priceImpact: Int, val notificationConfig: NotificationConfig) : SwapWarning + data class TooSmallAmountWarning(val notificationConfig: NotificationConfig) : SwapWarning + data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning } enum class GenericWarningType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 76844c8cd2..a4fa64ab24 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -19,4 +19,5 @@ data class UiActions( val onSelectFeeType: (TxFee) -> Unit, val onProviderClick: (String) -> Unit, val onProviderSelect: (String) -> Unit, + val onBuyClick: () -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index 78feaf99a9..2d1d66ef31 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -1,5 +1,7 @@ package com.tangem.feature.swap.models.states +import com.tangem.core.ui.extensions.TextReference + sealed class ProviderState { abstract val onProviderClick: ((String) -> Unit)? @@ -32,7 +34,8 @@ sealed class ProviderState { val name: String, val type: String, val iconUrl: String, - val alertText: String, + val alertText: TextReference, + val selectionType: SelectionType, override val onProviderClick: ((String) -> Unit)? = null, ) : ProviderState() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 6d2bab0f4f..c0743867bd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.ProviderState @@ -99,7 +100,8 @@ private fun ChooseProviderBottomSheet_Preview() { name = "1inch", type = "DEX", iconUrl = "", - alertText = "Unavailable", + selectionType = ProviderState.SelectionType.SELECT, + alertText = stringReference("Unavailable"), ), ) TangemTheme(isDark = false) { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 5d75293da0..8e51119147 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -13,12 +13,15 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.states.ProviderState @@ -64,6 +67,7 @@ fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected ProviderUnavailableState( state = state, modifier = modifier, + isSelected = isSelected, ) } is ProviderState.Empty -> { @@ -127,31 +131,42 @@ private fun ProviderContentState( } } Row( - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + modifier = Modifier.padding( + top = TangemTheme.dimens.spacing8, + end = TangemTheme.dimens.spacing56, + ), ) { Text( text = state.rate, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) if (state.percentLowerThenBest != null) { Text( - text = "${state.percentLowerThenBest}%", // todo add to strings + text = "${state.percentLowerThenBest}%", style = TangemTheme.typography.body2, color = TangemTheme.colors.text.warning, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) } } } } - ProviderChevron(state = state, isSelected = isSelected) + ProviderChevron(selectionType = state.selectionType, isSelected = isSelected) } } @Composable -private fun ProviderUnavailableState(state: ProviderState.Unavailable, modifier: Modifier = Modifier) { +private fun ProviderUnavailableState( + state: ProviderState.Unavailable, + isSelected: Boolean, + modifier: Modifier = Modifier, +) { Box(modifier = modifier.fillMaxWidth()) { Row { val (alpha, colorFilter) = GRAY_SCALE_ALPHA to GrayscaleColorFilter @@ -195,13 +210,15 @@ private fun ProviderUnavailableState(state: ProviderState.Unavailable, modifier: ) } Text( - text = state.alertText, + text = state.alertText.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) } } + + ProviderChevron(selectionType = state.selectionType, isSelected = isSelected) } } @@ -247,8 +264,8 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) { } @Composable -private fun BoxScope.ProviderChevron(state: ProviderState.Content, isSelected: Boolean) { - when (state.selectionType) { +private fun BoxScope.ProviderChevron(selectionType: ProviderState.SelectionType, isSelected: Boolean) { + when (selectionType) { ProviderState.SelectionType.NONE -> { /* no-op */ } @@ -396,7 +413,8 @@ private fun ProviderItem_Unavailable_Preview() { name = "1inch", type = "DEX", iconUrl = "", - alertText = "Unavailable", + selectionType = ProviderState.SelectionType.SELECT, + alertText = stringReference("Unavailable"), ) Column { TangemTheme(isDark = false) { @@ -408,5 +426,11 @@ private fun ProviderItem_Unavailable_Preview() { TangemTheme(isDark = true) { ProviderItemBlock(state = state) } + + SpacerH24() + + TangemTheme(isDark = true) { + ProviderItem(state = state, isSelected = true) + } } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 054e89a4bc..dd313d462a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -6,10 +6,7 @@ import com.tangem.common.Provider import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency @@ -125,7 +122,7 @@ internal class StateBuilder( notificationConfig = NotificationConfig( title = stringReference("No tokens"), subtitle = stringReference("Swap tokens not available"), - iconResId = R.drawable.ic_alert_24, + iconResId = R.drawable.img_attention_20, ), ), ), @@ -213,6 +210,18 @@ internal class StateBuilder( ), ) } + if (quoteModel.permissionState !is PermissionDataState.PermissionReadyForRequest && + !quoteModel.preparedSwapConfigState.isFeeEnough + ) { + warnings.add( + SwapWarning.UnableToCoverFeeWarning( + createUnableToCoverFeeNotificationConfig( + fromToken = fromToken, + onBuyClick = actions.onBuyClick, + ), + ), + ) + } if (!quoteModel.preparedSwapConfigState.isBalanceEnough) { warnings.add(SwapWarning.InsufficientFunds) } @@ -605,14 +614,23 @@ internal class StateBuilder( fun showSelectProviderBottomSheet( uiState: SwapStateHolder, selectedProviderId: String, + bestRatedProviderId: String, providersStates: Map, + unavailableProviders: List, onDismiss: () -> Unit, ): SwapStateHolder { + val availableProvidersStates = providersStates.entries.mapNotNull { + it.convertToProviderState(bestRatedProviderId, actions.onProviderSelect) + } + val unavailableProviderStates = unavailableProviders.map { + it.convertToUnavailableProviderState( + alertText = stringReference("Unavailable for pair"), + selectionType = ProviderState.SelectionType.NONE, + ) + } val config = ChooseProviderBottomSheetConfig( selectedProviderId = selectedProviderId, - providers = providersStates.entries - .mapNotNull { it.convertToProviderState(actions.onProviderSelect) } - .toImmutableList(), + providers = (availableProvidersStates + unavailableProviderStates).toImmutableList(), ) return uiState.copy( bottomSheetConfig = TangemBottomSheetConfig( @@ -703,21 +721,27 @@ internal class StateBuilder( } private fun Map.Entry.convertToProviderState( + bestRatedProviderId: String, onProviderSelect: (String) -> Unit, ): ProviderState? { val provider = this.key return when (val state = this.value) { is SwapState.EmptyAmountState -> null - is SwapState.QuotesLoadedState -> provider.convertToContentClickableProviderState( - fromTokenInfo = state.fromTokenInfo, - toTokenInfo = state.toTokenInfo, + is SwapState.QuotesLoadedState -> provider.convertToContentSelectableProviderState( + isBestRate = provider.providerId == bestRatedProviderId, + state = state, onProviderClick = onProviderSelect, selectionType = ProviderState.SelectionType.SELECT, ) - is SwapState.SwapError -> null + is SwapState.SwapError -> provider.convertToUnavailableProviderState( + alertText = stringReference("Available from"), + selectionType = ProviderState.SelectionType.NONE, + onProviderClick = onProviderSelect, + ) } } + // region warnings private fun createPermissionNotificationConfig(fromTokenSymbol: String): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.swapping_permission_header), @@ -737,6 +761,25 @@ internal class StateBuilder( ) } + private fun createUnableToCoverFeeNotificationConfig( + fromToken: CryptoCurrency, + onBuyClick: () -> Unit, + ): NotificationConfig { + // todo add strings + return NotificationConfig( + title = stringReference("Unable to cover ${fromToken.name} fee"), + subtitle = stringReference( + "To make transaction you need to deposit some ${fromToken.name} ${fromToken.symbol}", + ), + iconResId = fromToken.networkIconResId, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = stringReference("Buy ${fromToken.name}"), + onClick = onBuyClick, + ), + ) + } + // end region + private fun getShortAddressValue(fullAddress: String): String { check(fullAddress.length > ADDRESS_MIN_LENGTH) { "Invalid address" } val firstAddressPart = fullAddress.substring(startIndex = 0, endIndex = ADDRESS_FIRST_PART_LENGTH) @@ -774,6 +817,58 @@ internal class StateBuilder( ) } + private fun SwapProvider.convertToContentSelectableProviderState( + isBestRate: Boolean, + state: SwapState.QuotesLoadedState, + selectionType: ProviderState.SelectionType, + onProviderClick: (String) -> Unit, + ): ProviderState { + val fromTokenInfo = state.fromTokenInfo + val toTokenInfo = state.toTokenInfo + val rate = toTokenInfo.tokenAmount.value.divide( + fromTokenInfo.tokenAmount.value, + toTokenInfo.cryptoCurrencyStatus.currency.decimals, + RoundingMode.HALF_UP, + ) + val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol + val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol + val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" + val additionalBadge = if (state.permissionState is PermissionDataState.PermissionReadyForRequest) { + ProviderState.AdditionalBadge.PermissionRequired + } else if (isBestRate) { + ProviderState.AdditionalBadge.BestTrade + } else { + ProviderState.AdditionalBadge.Empty + } + return ProviderState.Content( + id = this.providerId, + name = this.name, + iconUrl = this.imageLarge, + type = this.type.toString(), + rate = rateString, + additionalBadge = additionalBadge, + selectionType = selectionType, + percentLowerThenBest = null, + onProviderClick = onProviderClick, + ) + } + + private fun SwapProvider.convertToUnavailableProviderState( + alertText: TextReference, + selectionType: ProviderState.SelectionType, + onProviderClick: ((String) -> Unit)? = null, + ): ProviderState { + return ProviderState.Unavailable( + id = this.providerId, + name = this.name, + iconUrl = this.imageLarge, + type = this.type.toString(), + selectionType = selectionType, + alertText = alertText, + onProviderClick = onProviderClick, + ) + } + private fun CryptoCurrencyStatus.getFormattedAmount(): String { val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index bf382cbc41..95fd4ce997 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -256,6 +256,7 @@ private fun SwapWarnings(warnings: List) { is SwapWarning.HighPriceImpact -> { Notification( config = warning.notificationConfig, + iconTint = TangemTheme.colors.icon.warning, ) } is SwapWarning.PermissionNeeded -> { @@ -282,6 +283,17 @@ private fun SwapWarnings(warnings: List) { config = warning.notificationConfig, ) } + is SwapWarning.TooSmallAmountWarning -> { + Notification( + config = warning.notificationConfig, + iconTint = TangemTheme.colors.icon.warning, + ) + } + is SwapWarning.UnableToCoverFeeWarning -> { + Notification( + config = warning.notificationConfig, + ) + } else -> {} } SpacerH8() @@ -379,7 +391,7 @@ private val state = SwapStateHolder( notificationConfig = NotificationConfig( title = stringReference("No tokens"), subtitle = stringReference("Swap tokens not available"), - iconResId = R.drawable.ic_alert_24, + iconResId = R.drawable.img_attention_20, ), ), ), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index d3121006ab..21c8322d64 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -17,7 +17,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor -import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider @@ -31,11 +30,14 @@ import com.tangem.feature.swap.ui.StateBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.isNullOrZero import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber +import java.math.BigDecimal +import java.math.RoundingMode import java.text.DecimalFormat import java.text.NumberFormat import java.util.Locale @@ -287,29 +289,62 @@ internal class SwapViewModel @Inject constructor( } is SwapState.SwapError -> { Timber.e("SwapError when loading quotes ${state.error}") + // todo handle when change token and error uiState = stateBuilder.mapError(uiState, state.error) { startLoadingQuotesFromLastState() } } } } private fun updateLoadedQuotes(state: Map): Pair { - val selectedSwapProvider = selectProvider(state) + val nonEmptyStates = state.filter { it.value !is SwapState.EmptyAmountState } + val selectedSwapProvider = if (nonEmptyStates.isNotEmpty()) { + selectProvider(state) + } else { + null + } dataState = dataState.copy( selectedProvider = selectedSwapProvider, lastLoadedSwapStates = state, ) - return state.entries.first { it.key == selectedSwapProvider }.toPair() + selectedSwapProvider?.let { + return state.entries.first { it.key == selectedSwapProvider }.toPair() + } + return state.entries.first().toPair() } private fun selectProvider(state: Map): SwapProvider { - val currentSelected = dataState.selectedProvider - return if (currentSelected != null && state.keys.contains(currentSelected)) { - currentSelected + val stateSuccess = state + .filter { it.value is SwapState.QuotesLoadedState } + .mapValues { it.value as SwapState.QuotesLoadedState } + return if (stateSuccess.isNotEmpty()) { + val currentSelected = dataState.selectedProvider + if (currentSelected != null && state.keys.contains(currentSelected)) { + currentSelected + } else { + findBestQuoteProvider(stateSuccess) ?: stateSuccess.keys.first() + } } else { state.keys.first() } } + private fun findBestQuoteProvider(state: Map): SwapProvider? { + // finding best quotes + return state.mapValues { + if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && + !it.value.toTokenInfo.amountFiat.isNullOrZero() + ) { + it.value.fromTokenInfo.amountFiat.divide( + it.value.toTokenInfo.amountFiat, + it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals, + RoundingMode.HALF_UP, + ) + } else { + BigDecimal.ZERO + } + }.minByOrNull { it.value }?.key + } + private fun fillLoadedDataState( state: SwapState.QuotesLoadedState, permissionState: PermissionDataState, @@ -478,7 +513,6 @@ internal class SwapViewModel @Inject constructor( private fun onTokenSelect(id: String) { val tokens = dataState.tokensDataState ?: return - val foundToken = if (isOrderReversed) { tokens.fromGroup.available.firstOrNull { it.currencyStatus.currency.id.value == id @@ -505,6 +539,7 @@ internal class SwapViewModel @Inject constructor( dataState = dataState.copy( fromCryptoCurrency = fromToken, toCryptoCurrency = toToken, + selectedProvider = null, ) startLoadingQuotes( fromToken = fromToken, @@ -675,10 +710,17 @@ internal class SwapViewModel @Inject constructor( setupLoadedState(selectedProvider, updatedState, fromToken) } }, - onProviderClick = { + onProviderClick = { providerId -> + val states = dataState.lastLoadedSwapStates + .filter { it.value is SwapState.QuotesLoadedState } + .mapValues { it.value as SwapState.QuotesLoadedState } + val bestRatedProviderId = findBestQuoteProvider(states)?.providerId ?: providerId + val unavailableProviders = getUnavailableProvidersFor(dataState.lastLoadedSwapStates) uiState = stateBuilder.showSelectProviderBottomSheet( uiState = uiState, - selectedProviderId = it, + selectedProviderId = providerId, + bestRatedProviderId = bestRatedProviderId, + unavailableProviders = unavailableProviders, providersStates = dataState.lastLoadedSwapStates, ) { uiState = stateBuilder.dismissBottomSheet(uiState) } }, @@ -695,6 +737,7 @@ internal class SwapViewModel @Inject constructor( ) } }, + onBuyClick = {}, ) } @@ -736,6 +779,25 @@ internal class SwapViewModel @Inject constructor( return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers ?: emptyList() } + private fun getAllProviders(): List { + dataState.tokensDataState?.let { data -> + val allProviders = + data.fromGroup.available.flatMap { it.providers } + data.toGroup.available.flatMap { it.providers } + return allProviders.distinct() + } ?: return emptyList() + } + + private fun getUnavailableProvidersFor(state: Map): List { + return getAllProviders() + .mapNotNull { + if (state.containsKey(it)) { + null + } else { + it + } + } + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" From ed053eb38f537bb8e4bd4f71e71ca853a9c2da71 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 21:34:40 +0300 Subject: [PATCH 072/139] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 17 +++++-- core/res/src/main/res/values/strings.xml | 47 ++++++++++++------- .../data/tokens/utils/NetworkOperations.kt | 1 + .../com/tangem/domain/tokens/model/Network.kt | 1 + .../tangem/domain/tokens/mock/MockNetworks.kt | 6 +++ .../state/fields/SendAmountFieldConverter.kt | 2 +- .../feature/swap/ui/ChooseFeeBottomSheet.kt | 7 ++- .../swap/ui/ChooseProviderBottomSheet.kt | 6 ++- .../tangem/feature/swap/ui/ProviderItem.kt | 1 + .../tangem/feature/swap/ui/StateBuilder.kt | 37 +++++++++------ 10 files changed, 83 insertions(+), 42 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 77fd07304f..194da3dfb2 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -66,6 +66,7 @@ Одобрение Внимание Баланс: %s + Баланс биометрическую аутентификацию биометрией Купить @@ -85,9 +86,11 @@ Включено Ошибка Обменять - Посмотреть историю транзакций Обозреватель + Посмотреть историю + Посмотреть историю транзакций Обозреватель + Комиссия Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции. Свое Быстро @@ -126,7 +129,6 @@ Отправить Успешно Обмен - Комиссия условия участия Ошибка транзакции Транзакции @@ -192,14 +194,18 @@ Мои токены У вас нет добавленных токенов. Добавьте токены для обмена Недоступен для обмена с %s + Статус Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами Выберите провайдера Чтобы узнать причину, посетите сайт провайдера Операция не выполнена провайдером Посетите сайт провайдера для проверки Провайдер: требуется верификация + Получение наилучших курсов... Провайдер Лучший курс + Доступно с %s + Недоступно для этой пары Требуется разрешение Информация ниже не является обязательной. Вы можете стереть её, если хотите. Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. @@ -273,7 +279,6 @@ Введенные коды доступа не совпадают Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить? Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас. - Баланс Добавить резервную карту Сканировать карту #%d Создать резервную копию @@ -634,12 +639,18 @@ Нравится Понятно! Очень круто! + Обновить Вы находитесь в режиме демо Демо режим включен Отсканированная вами карта является картой разработчика. Не используйте ее для создания своего кошелька. Не для пользователя! Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены. Для работы с сетью необходим депозит + У вас в списке нет монет доступных для обмена с %s + Нет доступных токенов для обмена + Cервис временно недоступен + Пожалуйста, измените сумму для обмена + Сумма для обмена должна быть не менее %s Возможно, данная карта - образец или подделка Ошибка проверки подлинности На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index ffc1858e33..8d4ff0b882 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -64,6 +64,7 @@ Approval Attention Balance: %s + Balance biometric authentication biometrics Buy @@ -84,8 +85,10 @@ Error Exchange Explore + Explore history Explore transaction history Explorer + Fee Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority. Custom Fast @@ -125,7 +128,6 @@ Submit Success Swap - Fee terms and conditions Transaction failed Transactions @@ -186,14 +188,16 @@ Terms of Service Oops, the current version of the application is not ready to work with this card, please check for updates. You have used a card from another wallet. Tap the card associated with this wallet - You Receive - You Send + You receive + You send My tokens - You don\'t have any added tokens yet. Add tokens via Market to swap - Unavailable for swap from %s - Providers facilitate transactions, ensuring smooth and efficient token exchanges - Choose Provider + You haven\'t added any tokens yet. Add tokens via Market to swap + Cannot be swapped for %s + Status + Providers facilitate transactions, ensuring smooth and efficient token swaps + Choose provider Estimated amount + Exchange by %s Visit provider’s website to see why Operation failed by provider Visit provider’s website for verification @@ -216,7 +220,8 @@ Go to provider Provider Best rate - Exchange by %s + Available from %s + Unavailable for this pair Permission Needed The following information is optional. You can erase it if you don\'t want to share it. Tell us what functions you are missing, and we will try to help you. @@ -291,7 +296,6 @@ Entered access code didn\'t match the initial access code You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process? The backup process is partly complete. You can\'t exit it now. - Balance Add a backup card Scan the card #%d Backup now @@ -437,7 +441,6 @@ Invalid Memo. It won\'t be added to the transaction. Tag Memo - Insufficient funds for transfer Include fee Low Normal @@ -513,7 +516,7 @@ Meet Tangem Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services Web 3.0 Compatible - Approvals are considered an industry standard across all decentralized exchanges and protect your wallet from being accessed by a smart contract without your permission. By design, smart contracts can\'t access your tokens unless you approve access from your end. By \"unlocking\" your tokens, you are give permission to the 1inch smart contract to spend your assets. The miners of the network are compensated with a gas fee (paid by you) to record this action on the blockchain. Once permission has been granted you will be able to swap your token. + All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. Approve Error: %s There was an error. Please try again. @@ -521,25 +524,25 @@ High price impact! Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds - Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first. + Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first. Transaction in progress... Waiting Approve Current transaction - The token approval network fee will be charged to confirm that you are the one allowing your token to be used for the exchange. + The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Give Permission Specify the approve limit for the selected token Amount %s Spender - Your Wallet - To continue you need to allow 1inch smart contracts to use your %s + Your wallet + To continue, grant 1inch smart contracts permission to use your %s Unlimited - View in Explorer - In progress You swap You receive + View in Explorer + In progress Swap - Swap of %s to + Swap %s for Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product. Other tokens Choose token @@ -661,12 +664,20 @@ Like it Ok, Got it! Really cool! + Refresh You are currently in the Demo mode Demo mode active The card you scanned is a developer card. Do not use it to create your wallet. Not for users! %1$s network requires an Existential Deposit. If your account drops below %2$s, it will be deactivated, and any remaining funds will be destroyed. Network requires Existential Deposit + You do not have any %s exchangeable coins in your list + No available tokens to swap + To make a transaction you need to deposit some %1$s %2$s + Unable to cover %s fee + Service temporary unavailable + Please change the amount to swap + The amount to swap must be at least %s This card might be a production sample or counterfeit Authenticity check failed Only %s signatures are left on this card. You must withdraw all of your funds. diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt index f5a1bf13df..ae8d8d9373 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt @@ -26,6 +26,7 @@ internal fun getNetwork( name = blockchain.fullName, isTestnet = blockchain.isTestnet(), derivationPath = getNetworkDerivationPath(blockchain, extraDerivationPath, derivationStyleProvider), + currencySymbol = blockchain.currency, standardType = getNetworkStandardType(blockchain), ) } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt index c57d5beee8..17274cfbe9 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt @@ -22,6 +22,7 @@ data class Network( val id: ID, val backendId: String, val name: String, + val currencySymbol: String, val derivationPath: DerivationPath, val isTestnet: Boolean, val standardType: StandardType, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 979479e010..7b61592137 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -18,6 +18,8 @@ internal object MockNetworks { name = "Network One", isTestnet = false, standardType = Network.StandardType.ERC20, + backendId = "network1", + currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, ) @@ -26,6 +28,8 @@ internal object MockNetworks { name = "Network Two", isTestnet = false, standardType = Network.StandardType.ERC20, + backendId = "network1", + currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, ) @@ -34,6 +38,8 @@ internal object MockNetworks { name = "Network Three", isTestnet = false, standardType = Network.StandardType.ERC20, + backendId = "network1", + currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt index 4b027a624d..9ab26bd83d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt @@ -24,7 +24,7 @@ internal class SendAmountFieldConverter( ), placeholder = TextReference.Str(DEFAULT_VALUE), isError = false, - error = TextReference.Res(R.string.send_insufficient_funds), + error = TextReference.Res(R.string.swapping_insufficient_funds), ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 4556f2d785..d0f47a62a2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -7,6 +7,7 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SpacerH24 @@ -35,7 +36,7 @@ private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { modifier = Modifier.background(TangemTheme.colors.background.primary), ) { Text( - text = "Choose fee", // todo replace with strings + text = stringResource(R.string.common_fee_selector_title), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, modifier = Modifier @@ -53,9 +54,7 @@ private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { FeeItemsBlock(content) } Text( - text = "Network transaction fees are small charges paid to support network security, incentivize " + - "validators," + - " allocate resources, and determine transaction priority.", // todo replace with strings + text = stringResource(R.string.common_fee_selector_footer), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, modifier = Modifier diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index c0743867bd..f11e395923 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -10,6 +10,7 @@ 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.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet @@ -18,6 +19,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.ProviderState +import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.toImmutableList @Composable @@ -33,7 +35,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC modifier = Modifier.background(TangemTheme.colors.background.primary), ) { Text( - text = "Choose provider", + text = stringResource(R.string.express_choose_providers_title), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, modifier = Modifier @@ -41,7 +43,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC .align(Alignment.CenterHorizontally), ) Text( - text = "Providers facilitate transactions, ensuring smooth and efficient token exchanges", + text = stringResource(R.string.express_choose_providers_subtitle), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, modifier = Modifier diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 8e51119147..99595d6398 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -76,6 +76,7 @@ fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected } } +@Suppress("LongMethod") @Composable private fun ProviderContentState( state: ProviderState.Content, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index dd313d462a..43cb364d2c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -120,8 +120,11 @@ internal class StateBuilder( warnings = listOf( SwapWarning.NoAvailableTokensToSwap( notificationConfig = NotificationConfig( - title = stringReference("No tokens"), - subtitle = stringReference("Swap tokens not available"), + title = resourceReference(R.string.warning_express_no_exchangeable_coins_title), + subtitle = resourceReference( + id = R.string.warning_express_no_exchangeable_coins_description, + formatArgs = wrappedList(fromToken.currency.name), + ), iconResId = R.drawable.img_attention_20, ), ), @@ -210,8 +213,9 @@ internal class StateBuilder( ), ) } - if (quoteModel.permissionState !is PermissionDataState.PermissionReadyForRequest && - !quoteModel.preparedSwapConfigState.isFeeEnough + if (quoteModel.preparedSwapConfigState.isAllowedToSpend && + !quoteModel.preparedSwapConfigState.isFeeEnough && + quoteModel.preparedSwapConfigState.isBalanceEnough ) { warnings.add( SwapWarning.UnableToCoverFeeWarning( @@ -439,7 +443,7 @@ internal class StateBuilder( return FeeItemState.Content( feeType = feeType, - title = stringReference("Fee"), // todo replace with string + title = resourceReference(R.string.common_fee_label), amountCrypto = fee.feeCryptoFormatted, symbolCrypto = fee.cryptoSymbol, amountFiatFormatted = fee.feeFiatFormatted, @@ -611,6 +615,7 @@ internal class StateBuilder( ) } + @Suppress("LongParameterList") fun showSelectProviderBottomSheet( uiState: SwapStateHolder, selectedProviderId: String, @@ -624,7 +629,7 @@ internal class StateBuilder( } val unavailableProviderStates = unavailableProviders.map { it.convertToUnavailableProviderState( - alertText = stringReference("Unavailable for pair"), + alertText = resourceReference(R.string.express_provider_not_available), selectionType = ProviderState.SelectionType.NONE, ) } @@ -701,7 +706,7 @@ internal class StateBuilder( return listOf( FeeItemState.Content( feeType = this.normalFee.feeType, - title = stringReference("Fee"), // todo replace with string + title = resourceReference(R.string.common_fee_label), amountCrypto = this.normalFee.feeCryptoFormatted, symbolCrypto = this.normalFee.cryptoSymbol, amountFiatFormatted = this.normalFee.feeFiatFormatted, @@ -710,7 +715,7 @@ internal class StateBuilder( ), FeeItemState.Content( feeType = this.priorityFee.feeType, - title = stringReference("Fee"), // todo replace with string + title = resourceReference(R.string.common_fee_label), amountCrypto = this.priorityFee.feeCryptoFormatted, symbolCrypto = this.priorityFee.cryptoSymbol, amountFiatFormatted = this.priorityFee.feeFiatFormatted, @@ -733,8 +738,9 @@ internal class StateBuilder( onProviderClick = onProviderSelect, selectionType = ProviderState.SelectionType.SELECT, ) + // todo handle error is SwapState.SwapError -> provider.convertToUnavailableProviderState( - alertText = stringReference("Available from"), + alertText = resourceReference(R.string.express_provider_min_amount, wrappedList("10")), selectionType = ProviderState.SelectionType.NONE, onProviderClick = onProviderSelect, ) @@ -765,15 +771,18 @@ internal class StateBuilder( fromToken: CryptoCurrency, onBuyClick: () -> Unit, ): NotificationConfig { - // todo add strings return NotificationConfig( - title = stringReference("Unable to cover ${fromToken.name} fee"), - subtitle = stringReference( - "To make transaction you need to deposit some ${fromToken.name} ${fromToken.symbol}", + title = resourceReference( + R.string.warning_express_not_enough_fee_for_token_tx_title, + wrappedList(fromToken.network.name), + ), + subtitle = resourceReference( + R.string.warning_express_not_enough_fee_for_token_tx_description, + wrappedList(fromToken.network.name, fromToken.network.currencySymbol), ), iconResId = fromToken.networkIconResId, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = stringReference("Buy ${fromToken.name}"), + text = resourceReference(R.string.common_buy_currency, wrappedList(fromToken.name)), onClick = onBuyClick, ), ) From 78497e41a8edab1e567325b8f4c1a4ad383462c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 23:00:19 +0200 Subject: [PATCH 073/139] Updated on 2026-08-14 --- .../api/common/response/ApiResponseError.kt | 6 ++- .../api/common/response/ResponseExt.kt | 2 +- .../models/response/ExpressErrorResponse.kt | 38 ++++++++++++++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 17 +++++-- .../swap/converters/ErrorsDataConverter.kt | 50 +++++++++++++++++++ .../tangem/feature/swap/di/SwapDataModule.kt | 20 +++++++- .../feature/swap/domain/models/DataError.kt | 38 +++++++++++--- .../models/data/AggregatedSwapDataModel.kt | 2 +- .../tangem/feature/swap/ui/StateBuilder.kt | 2 +- 9 files changed, 159 insertions(+), 16 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt 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 5e88155058..e54744c876 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 @@ -12,7 +12,11 @@ sealed class ApiResponseError : Exception() { * @property code The HTTP status code. * @property message A human-readable message describing the error. */ - data class HttpException(val code: Code, override val message: String?) : ApiResponseError() { + data class HttpException( + val code: Code, + override val message: String?, + val errorBody: String? + ) : ApiResponseError() { // region Error Codes enum class Code(val code: Int) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt index 0ddb0ffa36..718392b0d9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt @@ -16,7 +16,7 @@ internal fun Response.toSafeApiResponse(): ApiResponse { val e = if (code == null) { ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}")) } else { - ApiResponseError.HttpException(code, message()) + ApiResponseError.HttpException(code, message(), errorBody()?.string()) } apiError(e) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt new file mode 100644 index 0000000000..e6adcd593b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt @@ -0,0 +1,38 @@ +package com.tangem.datasource.api.express.models.response + +import com.squareup.moshi.Json +import java.math.BigDecimal +import java.math.BigInteger + +data class ExpressErrorResponse( + @Json(name = "error") + val error: ExpressError, +) + +data class ExpressError( + @Json(name = "code") + val code: Int, + + @Json(name = "description") + val description: String?, + + @Json(name = "value") + val value: ExpressErrorValue?, +) + +data class ExpressErrorValue( + @Json(name = "minAmount") + val minAmount: BigDecimal?, + + @Json(name = "decimals") + val decimals: Int?, + + @Json(name = "currentAllowance") + val currentAllowance: BigDecimal?, + + @Json(name = "receivedFromDecimals") + val receivedFromDecimals: Int?, + + @Json(name = "expressFromDecimals") + val expressFromDecimals: Int?, +) \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 43010d791d..8c53c20831 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -1,11 +1,13 @@ package com.tangem.feature.swap +import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Approver import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.extensions.Result import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.request.PairsRequestBody @@ -23,10 +25,10 @@ import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* -import com.tangem.feature.swap.domain.models.mapErrors import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.withContext @@ -43,6 +45,7 @@ internal class SwapRepositoryImpl @Inject constructor( private val configManager: ConfigManager, private val walletManagersFacade: WalletManagersFacade, private val walletsStateHolder: WalletsStateHolder, + private val errorsDataConverter: ErrorsDataConverter, ) : SwapRepository { private val tokensConverter = TokensConverter() @@ -165,7 +168,7 @@ internal class SwapRepositoryImpl @Inject constructor( ), ) } catch (ex: Exception) { - AggregatedSwapDataModel(null, mapErrors(ex.message)) + AggregatedSwapDataModel(null, getDataError(ex)) } } } @@ -204,7 +207,7 @@ internal class SwapRepositoryImpl @Inject constructor( dataModel = expressDataConverter.convert(response), ) } catch (ex: Exception) { - AggregatedSwapDataModel(null, mapErrors(ex.message)) + AggregatedSwapDataModel(null, getDataError(ex)) } } } @@ -301,6 +304,14 @@ internal class SwapRepositoryImpl @Inject constructor( ) } + private fun getDataError(ex: Exception) : DataError { + return if (ex is ApiResponseError.HttpException) { + errorsDataConverter.convert(ex.errorBody ?: "") + } else { + DataError.UnknownError() + } + } + companion object { // TODO("get this ids from blockchain enum later") private const val OPTIMISM_ID = "optimistic-ethereum" diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt new file mode 100644 index 0000000000..c1ff51fd25 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.swap.converters + +import com.squareup.moshi.JsonAdapter +import com.tangem.datasource.api.express.models.response.ExpressErrorResponse +import com.tangem.feature.swap.domain.models.DataError +import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.utils.converter.Converter + +internal class ErrorsDataConverter( + private val jsonAdapter: JsonAdapter, +) : Converter { + + override fun convert(errorBody: String): DataError { + try { + val errorResponse = jsonAdapter.fromJson(errorBody) + + val error = errorResponse?.error ?: return DataError.UnknownError() + + val dataError = when (error.code) { + 2010 -> DataError.BadRequest(code = error.code) + 2210 -> DataError.ExchangeProviderNotFoundError(code = error.code) + 2220 -> DataError.ExchangeProviderNotActiveError(code = error.code) + 2230 -> DataError.ExchangeProviderNotAvailableError(code = error.code) + 2240 -> DataError.ExchangeNotPossibleError(code = error.code) + 2250 -> DataError.ExchangeTooSmallAmountError( + code = error.code, + amount = SwapAmount( + requireNotNull(error.value?.minAmount), + requireNotNull(error.value?.decimals) + ) + ) + 2260 -> DataError.ExchangeNotEnoughAllowanceError( + code = error.code, + currentAllowance = requireNotNull(error.value?.currentAllowance) + ) + 2270 -> DataError.ExchangeNotEnoughBalanceError(code = error.code) + 2280 -> DataError.ExchangeInvalidAddressError(code = error.code) + 2290 -> DataError.ExchangeInvalidFromDecimalsError(code = error.code, + receivedFromDecimals = requireNotNull(error.value?.receivedFromDecimals), + expressFromDecimals = requireNotNull(error.value?.expressFromDecimals) + ) + else -> DataError.UnknownError() + } + + return dataError + } catch (e: Exception) { + return DataError.UnknownError() + } + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 261dcbb069..b52fd9d560 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -1,12 +1,17 @@ package com.tangem.feature.swap.di +import com.squareup.moshi.Moshi import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.api.oneinch.OneInchApiFactory import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.di.NetworkMoshi import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.feature.swap.SwapRepositoryImpl +import com.tangem.feature.swap.converters.ErrorsDataConverter +import com.tangem.feature.swap.converters.ExpressDataConverter import com.tangem.feature.swap.domain.SwapRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -17,11 +22,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class SwapDataModule { +internal class SwapDataModule { @Provides @Singleton - fun provideSwapRepository( + internal fun provideSwapRepository( tangemTechApi: TangemTechApi, tangemExpressApi: TangemExpressApi, oneInchApiFactory: OneInchApiFactory, @@ -29,6 +34,7 @@ class SwapDataModule { configManager: ConfigManager, walletManagerFacade: WalletManagersFacade, walletsStateHolder: WalletsStateHolder, + errorsDataConverter: ErrorsDataConverter, ): SwapRepository { return SwapRepositoryImpl( tangemTechApi = tangemTechApi, @@ -38,6 +44,16 @@ class SwapDataModule { configManager = configManager, walletManagersFacade = walletManagerFacade, walletsStateHolder = walletsStateHolder, + errorsDataConverter = errorsDataConverter, ) } + + @Provides + @Singleton + internal fun provideErrorsConverter( + @NetworkMoshi moshi: Moshi + ) : ErrorsDataConverter { + val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java) + return ErrorsDataConverter(jsonAdapter) + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index a00be2a2bc..accf6dd7d1 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -1,10 +1,34 @@ package com.tangem.feature.swap.domain.models -sealed class DataError { - object UnknownError : DataError() - data class Error(val message: String) : DataError() -} +import java.math.BigDecimal -fun mapErrors(error: String?): DataError { - return error?.let { DataError.Error(it) } ?: DataError.UnknownError -} \ No newline at end of file +sealed class DataError { + + abstract val code: Int + + data class BadRequest(override val code: Int) : DataError() + + data class ExchangeProviderNotFoundError(override val code: Int): DataError() + + data class ExchangeProviderNotActiveError(override val code: Int): DataError() + + data class ExchangeProviderNotAvailableError(override val code: Int): DataError() + + data class ExchangeNotPossibleError(override val code: Int): DataError() + + data class ExchangeTooSmallAmountError(override val code: Int, val amount: SwapAmount): DataError() + + data class ExchangeNotEnoughAllowanceError(override val code: Int, val currentAllowance: BigDecimal): DataError() + + data class ExchangeNotEnoughBalanceError(override val code: Int): DataError() + + data class ExchangeInvalidAddressError(override val code: Int): DataError() + + data class ExchangeInvalidFromDecimalsError( + override val code: Int, + val receivedFromDecimals: Int, + val expressFromDecimals: Int + ): DataError() + + data class UnknownError(override val code: Int = -1): DataError() +} diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt index 45a895181e..9f83feedf4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt @@ -11,5 +11,5 @@ import com.tangem.feature.swap.domain.models.DataError */ data class AggregatedSwapDataModel( val dataModel: T?, - val error: DataError = DataError.UnknownError, + val error: DataError = DataError.UnknownError(), ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 054e89a4bc..8dc00151bb 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -502,7 +502,7 @@ internal class StateBuilder( // todo use if needed later // DataError.InsufficientLiquidity -> TODO() // DataError.NoError -> TODO() - is DataError.Error -> addWarning(uiState, error.message, true, onClick) + is DataError.ExchangeTooSmallAmountError -> addWarning(uiState, error.amount.toString(), true, onClick) else -> addWarning(uiState, null, false) {} } } From 474cd508205dd5bc635c5962b01f4619ad02fbca Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 23:01:32 +0200 Subject: [PATCH 074/139] Updated on 2026-08-14 --- .../api/common/response/ApiResponseError.kt | 2 +- .../models/response/ExpressErrorResponse.kt | 1 - .../tangem/feature/swap/SwapRepositoryImpl.kt | 3 +-- .../swap/converters/ErrorsDataConverter.kt | 12 ++++++---- .../tangem/feature/swap/di/SwapDataModule.kt | 5 +--- .../swap/domain/di/SwapDomainModule.kt | 1 - .../feature/swap/domain/models/DataError.kt | 24 +++++++++---------- 7 files changed, 22 insertions(+), 26 deletions(-) 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 e54744c876..e342289f0c 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 @@ -15,7 +15,7 @@ sealed class ApiResponseError : Exception() { data class HttpException( val code: Code, override val message: String?, - val errorBody: String? + val errorBody: String?, ) : ApiResponseError() { // region Error Codes diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt index e6adcd593b..c0c39b0fb2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt @@ -2,7 +2,6 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json import java.math.BigDecimal -import java.math.BigInteger data class ExpressErrorResponse( @Json(name = "error") diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 8c53c20831..f4e943542b 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -1,6 +1,5 @@ package com.tangem.feature.swap -import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Approver import com.tangem.blockchain.common.Blockchain @@ -304,7 +303,7 @@ internal class SwapRepositoryImpl @Inject constructor( ) } - private fun getDataError(ex: Exception) : DataError { + private fun getDataError(ex: Exception): DataError { return if (ex is ApiResponseError.HttpException) { errorsDataConverter.convert(ex.errorBody ?: "") } else { diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index c1ff51fd25..db420a4033 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -10,6 +10,7 @@ internal class ErrorsDataConverter( private val jsonAdapter: JsonAdapter, ) : Converter { + @Suppress("MagicNumber") override fun convert(errorBody: String): DataError { try { val errorResponse = jsonAdapter.fromJson(errorBody) @@ -26,18 +27,19 @@ internal class ErrorsDataConverter( code = error.code, amount = SwapAmount( requireNotNull(error.value?.minAmount), - requireNotNull(error.value?.decimals) - ) + requireNotNull(error.value?.decimals), + ), ) 2260 -> DataError.ExchangeNotEnoughAllowanceError( code = error.code, - currentAllowance = requireNotNull(error.value?.currentAllowance) + currentAllowance = requireNotNull(error.value?.currentAllowance), ) 2270 -> DataError.ExchangeNotEnoughBalanceError(code = error.code) 2280 -> DataError.ExchangeInvalidAddressError(code = error.code) - 2290 -> DataError.ExchangeInvalidFromDecimalsError(code = error.code, + 2290 -> DataError.ExchangeInvalidFromDecimalsError( + code = error.code, receivedFromDecimals = requireNotNull(error.value?.receivedFromDecimals), - expressFromDecimals = requireNotNull(error.value?.expressFromDecimals) + expressFromDecimals = requireNotNull(error.value?.expressFromDecimals), ) else -> DataError.UnknownError() } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index b52fd9d560..4c34db7b17 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -11,7 +11,6 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.feature.swap.SwapRepositoryImpl import com.tangem.feature.swap.converters.ErrorsDataConverter -import com.tangem.feature.swap.converters.ExpressDataConverter import com.tangem.feature.swap.domain.SwapRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -50,9 +49,7 @@ internal class SwapDataModule { @Provides @Singleton - internal fun provideErrorsConverter( - @NetworkMoshi moshi: Moshi - ) : ErrorsDataConverter { + internal fun provideErrorsConverter(@NetworkMoshi moshi: Moshi): ErrorsDataConverter { val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java) return ErrorsDataConverter(jsonAdapter) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 734efb5f0d..dd81f44d66 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -14,7 +14,6 @@ import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* import com.tangem.feature.swap.domain.cache.SwapDataCacheImpl -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index accf6dd7d1..8ea211009c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -8,27 +8,27 @@ sealed class DataError { data class BadRequest(override val code: Int) : DataError() - data class ExchangeProviderNotFoundError(override val code: Int): DataError() + data class ExchangeProviderNotFoundError(override val code: Int) : DataError() - data class ExchangeProviderNotActiveError(override val code: Int): DataError() + data class ExchangeProviderNotActiveError(override val code: Int) : DataError() - data class ExchangeProviderNotAvailableError(override val code: Int): DataError() + data class ExchangeProviderNotAvailableError(override val code: Int) : DataError() - data class ExchangeNotPossibleError(override val code: Int): DataError() + data class ExchangeNotPossibleError(override val code: Int) : DataError() - data class ExchangeTooSmallAmountError(override val code: Int, val amount: SwapAmount): DataError() + data class ExchangeTooSmallAmountError(override val code: Int, val amount: SwapAmount) : DataError() - data class ExchangeNotEnoughAllowanceError(override val code: Int, val currentAllowance: BigDecimal): DataError() + data class ExchangeNotEnoughAllowanceError(override val code: Int, val currentAllowance: BigDecimal) : DataError() - data class ExchangeNotEnoughBalanceError(override val code: Int): DataError() + data class ExchangeNotEnoughBalanceError(override val code: Int) : DataError() - data class ExchangeInvalidAddressError(override val code: Int): DataError() + data class ExchangeInvalidAddressError(override val code: Int) : DataError() data class ExchangeInvalidFromDecimalsError( override val code: Int, val receivedFromDecimals: Int, - val expressFromDecimals: Int - ): DataError() + val expressFromDecimals: Int, + ) : DataError() - data class UnknownError(override val code: Int = -1): DataError() -} + data class UnknownError(override val code: Int = -1) : DataError() +} \ No newline at end of file From 0f536155181f7befd8e94c6e1fce4eb29a198694 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 23:05:42 +0200 Subject: [PATCH 075/139] Updated on 2026-08-14 --- .../com/tangem/feature/swap/SwapRepositoryImpl.kt | 2 +- .../feature/swap/converters/ErrorsDataConverter.kt | 14 +++++--------- .../tangem/feature/swap/domain/models/DataError.kt | 4 +++- .../domain/models/data/AggregatedSwapDataModel.kt | 2 +- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index f4e943542b..9ea783fc6b 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -307,7 +307,7 @@ internal class SwapRepositoryImpl @Inject constructor( return if (ex is ApiResponseError.HttpException) { errorsDataConverter.convert(ex.errorBody ?: "") } else { - DataError.UnknownError() + DataError.UnknownError } } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index db420a4033..5330219b1e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -11,13 +11,11 @@ internal class ErrorsDataConverter( ) : Converter { @Suppress("MagicNumber") - override fun convert(errorBody: String): DataError { + override fun convert(value: String): DataError { try { - val errorResponse = jsonAdapter.fromJson(errorBody) + val error = jsonAdapter.fromJson(value)?.error ?: return DataError.UnknownError - val error = errorResponse?.error ?: return DataError.UnknownError() - - val dataError = when (error.code) { + return when (error.code) { 2010 -> DataError.BadRequest(code = error.code) 2210 -> DataError.ExchangeProviderNotFoundError(code = error.code) 2220 -> DataError.ExchangeProviderNotActiveError(code = error.code) @@ -41,12 +39,10 @@ internal class ErrorsDataConverter( receivedFromDecimals = requireNotNull(error.value?.receivedFromDecimals), expressFromDecimals = requireNotNull(error.value?.expressFromDecimals), ) - else -> DataError.UnknownError() + else -> DataError.UnknownError } - - return dataError } catch (e: Exception) { - return DataError.UnknownError() + return DataError.UnknownError } } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index 8ea211009c..2c3a3e6af6 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -30,5 +30,7 @@ sealed class DataError { val expressFromDecimals: Int, ) : DataError() - data class UnknownError(override val code: Int = -1) : DataError() + object UnknownError : DataError() { + override val code: Int = -1 + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt index 9f83feedf4..45a895181e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt @@ -11,5 +11,5 @@ import com.tangem.feature.swap.domain.models.DataError */ data class AggregatedSwapDataModel( val dataModel: T?, - val error: DataError = DataError.UnknownError(), + val error: DataError = DataError.UnknownError, ) \ No newline at end of file From 93880c648228b5ff0dee4241d4392ec81d5538c4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 23:23:10 +0400 Subject: [PATCH 076/139] Updated on 2026-08-14 --- .../local/preferences/AppPreferencesStore.kt | 33 ++++++++++-- .../utils/AppPreferencesStoreExt.kt | 52 ++++++++++++++++--- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt index 7e401b9333..ae7bb4ee8c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import com.squareup.moshi.Moshi +import com.squareup.moshi.Types /** * Application preferences store. @@ -31,15 +32,39 @@ class AppPreferencesStore( return edit { transform(it) } } - /** Get nullable data [T] by string [key] from [MutablePreferences] */ + /** + * Get nullable data [T] by string [key] from [MutablePreferences] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see getObjectList + * */ inline fun MutablePreferences.getObject(key: Preferences.Key): T? { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types return this[key]?.let(adapter::fromJson) } - /** Set data [T] by string [key] to [MutablePreferences] */ + /** Get nullable list of data [T] by string [key] */ + inline fun MutablePreferences.getObjectList(key: Preferences.Key): List? { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + return this[key]?.let(adapter::fromJson) + } + + /** + * Set data [T] by string [key] to [MutablePreferences] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see setObjectList + * */ inline fun MutablePreferences.setObject(key: Preferences.Key, value: T) { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types + this[key] = adapter.toJson(value) + } + + /** Set list of data [T] by string [key] to [MutablePreferences] */ + inline fun MutablePreferences.setObjectList(key: Preferences.Key, value: List) { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) this[key] = adapter.toJson(value) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index 1ca1355871..974b36147a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.local.preferences.utils import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit +import com.squareup.moshi.Types import com.tangem.datasource.local.preferences.AppPreferencesStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull @@ -13,15 +14,27 @@ inline fun AppPreferencesStore.getObject(key: Preferences.Key AppPreferencesStore.getObject(key: Preferences.Key, default: T): Flow { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types return data.map { it[key]?.let(adapter::fromJson) ?: default } } -/** Get nullable data [T] by string [key] */ +/** + * Get nullable data [T] by string [key] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see getObjectListSync + * */ suspend inline fun AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key): T? { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types return data.firstOrNull() ?.get(key) ?.let(adapter::fromJson) @@ -39,8 +52,35 @@ suspend inline fun AppPreferencesStore.getObjectSyncOrDefault( ?: default } -/** Store data [value] by string [key] */ +/** + * Store data [value] by string [key] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see storeObjectList + * */ suspend inline fun AppPreferencesStore.storeObject(key: Preferences.Key, value: T) { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types edit { it[key] = adapter.toJson(value) } +} + +/** Store list of data [value] by string [key] */ +suspend inline fun AppPreferencesStore.storeObjectList(key: Preferences.Key, value: List) { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + edit { it[key] = adapter.toJson(value) } +} + +/** Get flow of list of data [T] by string [key]. If data is not found, it returns `null` */ +inline fun AppPreferencesStore.getObjectList(key: Preferences.Key): Flow?> { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + return data.map { it[key]?.let(adapter::fromJson) } +} + +/** Get nullable list of data [T] by string [key] */ +suspend inline fun AppPreferencesStore.getObjectListSync(key: Preferences.Key): List { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + return data.firstOrNull() + ?.get(key) + ?.let(adapter::fromJson) + .orEmpty() } \ No newline at end of file From 507c09beff593569aaa8d965ba35e23dec483cc0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 27 Nov 2023 23:23:42 +0400 Subject: [PATCH 077/139] Updated on 2026-08-14 --- .../java/com/tangem/data/card/DefaultCardRepository.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt index 7f0a56fd9a..84c73a8dc7 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt @@ -3,7 +3,7 @@ package com.tangem.data.card import com.tangem.datasource.local.card.UsedCardInfo import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.domain.card.repository.CardRepository import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow @@ -14,7 +14,7 @@ internal class DefaultCardRepository( ) : CardRepository { override fun wasCardScanned(cardId: String): Flow { - return appPreferencesStore.getObject>(key = PreferencesKeys.USED_CARDS_INFO_KEY) + return appPreferencesStore.getObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY) .map { savedCards -> savedCards?.any { it.cardId == cardId } ?: false } @@ -22,14 +22,14 @@ internal class DefaultCardRepository( override suspend fun setCardWasScanned(cardId: String) { appPreferencesStore.editData { mutablePreferences -> - val usedCards: List? = mutablePreferences.getObject( + val usedCards: List? = mutablePreferences.getObjectList( key = PreferencesKeys.USED_CARDS_INFO_KEY, ) val updatedUsedCards = usedCards?.updateCard(cardId) ?: listOf(UsedCardInfo(cardId = cardId, isScanned = true)) - mutablePreferences.setObject( + mutablePreferences.setObjectList( key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards, ) From 3c3af4cb34609ba71899ff8906352d2129edcbca Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 29 Nov 2023 11:59:04 +0300 Subject: [PATCH 078/139] Updated on 2026-08-14 --- .../local/preferences/AppPreferencesStore.kt | 33 ++++++++++-- .../utils/AppPreferencesStoreExt.kt | 52 ++++++++++++++++--- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt index 7e401b9333..ae7bb4ee8c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.MutablePreferences import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import com.squareup.moshi.Moshi +import com.squareup.moshi.Types /** * Application preferences store. @@ -31,15 +32,39 @@ class AppPreferencesStore( return edit { transform(it) } } - /** Get nullable data [T] by string [key] from [MutablePreferences] */ + /** + * Get nullable data [T] by string [key] from [MutablePreferences] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see getObjectList + * */ inline fun MutablePreferences.getObject(key: Preferences.Key): T? { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types return this[key]?.let(adapter::fromJson) } - /** Set data [T] by string [key] to [MutablePreferences] */ + /** Get nullable list of data [T] by string [key] */ + inline fun MutablePreferences.getObjectList(key: Preferences.Key): List? { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + return this[key]?.let(adapter::fromJson) + } + + /** + * Set data [T] by string [key] to [MutablePreferences] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see setObjectList + * */ inline fun MutablePreferences.setObject(key: Preferences.Key, value: T) { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types + this[key] = adapter.toJson(value) + } + + /** Set list of data [T] by string [key] to [MutablePreferences] */ + inline fun MutablePreferences.setObjectList(key: Preferences.Key, value: List) { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) this[key] = adapter.toJson(value) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index 1ca1355871..974b36147a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.local.preferences.utils import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit +import com.squareup.moshi.Types import com.tangem.datasource.local.preferences.AppPreferencesStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull @@ -13,15 +14,27 @@ inline fun AppPreferencesStore.getObject(key: Preferences.Key AppPreferencesStore.getObject(key: Preferences.Key, default: T): Flow { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types return data.map { it[key]?.let(adapter::fromJson) ?: default } } -/** Get nullable data [T] by string [key] */ +/** + * Get nullable data [T] by string [key] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see getObjectListSync + * */ suspend inline fun AppPreferencesStore.getObjectSyncOrNull(key: Preferences.Key): T? { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types return data.firstOrNull() ?.get(key) ?.let(adapter::fromJson) @@ -39,8 +52,35 @@ suspend inline fun AppPreferencesStore.getObjectSyncOrDefault( ?: default } -/** Store data [value] by string [key] */ +/** + * Store data [value] by string [key] + * + * Warning: This method cannot be used for T with parameterized types (e.g. List, Set, etc.). + * + * @see storeObjectList + * */ suspend inline fun AppPreferencesStore.storeObject(key: Preferences.Key, value: T) { - val adapter = moshi.adapter(T::class.java) + val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types edit { it[key] = adapter.toJson(value) } +} + +/** Store list of data [value] by string [key] */ +suspend inline fun AppPreferencesStore.storeObjectList(key: Preferences.Key, value: List) { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + edit { it[key] = adapter.toJson(value) } +} + +/** Get flow of list of data [T] by string [key]. If data is not found, it returns `null` */ +inline fun AppPreferencesStore.getObjectList(key: Preferences.Key): Flow?> { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + return data.map { it[key]?.let(adapter::fromJson) } +} + +/** Get nullable list of data [T] by string [key] */ +suspend inline fun AppPreferencesStore.getObjectListSync(key: Preferences.Key): List { + val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + return data.firstOrNull() + ?.get(key) + ?.let(adapter::fromJson) + .orEmpty() } \ No newline at end of file From fa6a757af56fe6db61ee262bc48a09b8cc6e3177 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 29 Nov 2023 15:10:44 +0300 Subject: [PATCH 079/139] Updated on 2026-08-14 --- .../components/appbar/AppBarWithBackButton.kt | 15 ++++--- .../feature/swap/domain/SwapInteractorImpl.kt | 39 ++++++++++------- .../feature/swap/domain/models/ui/TxState.kt | 1 + .../swap/models/SwapSuccessStateHolder.kt | 5 ++- .../tangem/feature/swap/ui/StateBuilder.kt | 43 +++++++++---------- .../feature/swap/ui/SwapSuccessScreen.kt | 19 +++----- .../feature/swap/viewmodels/SwapViewModel.kt | 7 ++- 7 files changed, 67 insertions(+), 62 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt index da053a53f7..afb57565f8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt @@ -3,14 +3,13 @@ package com.tangem.core.ui.components.appbar import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -49,7 +48,11 @@ fun AppBarWithBackButton( contentDescription = null, modifier = Modifier .size(size = TangemTheme.dimens.size24) - .clickable { onBackClick() }, + .clickable( + indication = rememberRipple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = onBackClick, + ), tint = TangemTheme.colors.icon.primary1, ) if (!text.isNullOrBlank()) { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index da5dd7b595..c219a29944 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -213,7 +213,10 @@ internal class SwapInteractorImpl @Inject constructor( return when (result) { is SendTxResult.Success -> { allowPermissionsHandler.addAddressToInProgress(permissionOptions.forTokenContractAddress) - TxState.TxSent(txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath) ?: "") + TxState.TxSent( + txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), + timestamp = System.currentTimeMillis(), + ) } SendTxResult.UserCancelledError -> TxState.UserCancelled is SendTxResult.BlockchainSdkError -> TxState.BlockchainError @@ -370,7 +373,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGet = currencyToGet, amount = amount, fee = fee, - providerId = swapProvider.providerId, + swapProvider = swapProvider, userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } @@ -455,7 +458,8 @@ internal class SwapInteractorImpl @Inject constructor( swapData.toTokenAmount, currencyToGet.symbol, ), - txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath) ?: "", + txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), + timestamp = System.currentTimeMillis(), ) } SendTxResult.UserCancelledError -> TxState.UserCancelled @@ -471,7 +475,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGet: CryptoCurrencyStatus, amount: SwapAmount, fee: TxFee, - providerId: String, + swapProvider: SwapProvider, userWalletId: UserWalletId, ): TxState { val exchangeData = repository.getExchangeData( @@ -481,7 +485,7 @@ internal class SwapInteractorImpl @Inject constructor( toNetwork = currencyToGet.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, - providerId = providerId, + providerId = swapProvider.providerId, rateType = RateType.FLOAT, toAddress = currencyToGet.value.networkAddress?.defaultAddress ?: "", ) @@ -501,14 +505,17 @@ internal class SwapInteractorImpl @Inject constructor( network = currencyToSend.currency.network, ) - return result.fold(ifLeft = { - when (it) { - is SendTransactionError.NetworkError -> TxState.NetworkError - is SendTransactionError.DataError -> TxState.BlockchainError - SendTransactionError.DemoCardError -> TxState.UnknownError - else -> TxState.UnknownError - } - }, ifRight = { + return result.fold( + ifLeft = { + when (it) { + is SendTransactionError.NetworkError -> TxState.NetworkError + is SendTransactionError.DataError -> TxState.BlockchainError + SendTransactionError.DemoCardError -> TxState.UnknownError + else -> TxState.UnknownError + } + }, + ifRight = { + val timestamp = System.currentTimeMillis() TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, @@ -521,9 +528,11 @@ internal class SwapInteractorImpl @Inject constructor( txAddress = userWalletManager.getLastTransactionHash( currencyToSend.currency.network.backendId, derivationPath, - ) ?: "", + ).orEmpty(), + timestamp = timestamp, ) - },) + }, + ) } @Deprecated("used in old swap mechanism") diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt index ca95f626e3..0a0f5b5b2a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt @@ -6,6 +6,7 @@ sealed class TxState { val fromAmount: String? = null, val toAmount: String? = null, val txAddress: String, + val timestamp: Long, ) : TxState() object UserCancelled : TxState() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt index 640c4809d3..814723462c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt @@ -2,14 +2,15 @@ package com.tangem.feature.swap.models import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.extensions.TextReference -import com.tangem.feature.swap.domain.models.domain.SwapProvider data class SwapSuccessStateHolder( val timestamp: Long, val txUrl: String, val fee: TextReference, val rate: TextReference, - val selectedProvider: SwapProvider, + val providerName: TextReference, + val providerType: TextReference, + val providerIcon: String, val fromTokenAmount: TextReference, val toTokenAmount: TextReference, val fromTokenFiatAmount: TextReference, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 7d965aa10f..3d230f08a8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -20,7 +20,6 @@ import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.presentation.R -import com.tangem.feature.swap.viewmodels.SwapProcessDataState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @@ -459,39 +458,37 @@ internal class StateBuilder( ) } + @Suppress("LongParameterList") fun createSuccessState( uiState: SwapStateHolder, txState: TxState.TxSent, - dataState: SwapProcessDataState, + fromAmount: BigDecimal, + toAmount: BigDecimal, txUrl: String, onSecondaryBtnClick: () -> Unit, ): SwapStateHolder { - val fromToken = requireNotNull(uiState.sendCardData as? SwapCardState.SwapCardData) - val toToken = requireNotNull(uiState.receiveCardData as? SwapCardState.SwapCardData) - val fromTokenIconState = fromToken.token?.let(iconStateConverter::convert) - val toTokenIconState = toToken.token?.let(iconStateConverter::convert) + val providerState = uiState.providerState as ProviderState.Content + val fromToken = requireNotNull((uiState.sendCardData as? SwapCardState.SwapCardData)?.token) + val toToken = requireNotNull((uiState.receiveCardData as? SwapCardState.SwapCardData)?.token) + val fromTokenIconState = iconStateConverter.convert(fromToken) + val toTokenIconState = iconStateConverter.convert(toToken) val fee = uiState.fee as? FeeItemState.Content ?: return uiState - val fromCryptoCurrencyStatus = requireNotNull(fromToken.token) - val toCryptoCurrencyStatus = requireNotNull(toToken.token) - val rate = txState.toAmount?.toBigDecimal()?.divide( - txState.fromAmount?.toBigDecimal(), - toCryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, - ) - val fromCurrencySymbol = fromCryptoCurrencyStatus.currency.symbol - val toCurrencySymbol = toCryptoCurrencyStatus.currency.symbol + val fromFiatAmount = getFormattedFiatAmount(fromToken.value.fiatRate?.multiply(fromAmount)) + val toFiatAmount = getFormattedFiatAmount(toToken.value.fiatRate?.multiply(toAmount)) return uiState.copy( successState = SwapSuccessStateHolder( - timestamp = System.currentTimeMillis(), + timestamp = txState.timestamp, txUrl = txUrl, - selectedProvider = requireNotNull(dataState.selectedProvider), + providerName = TextReference.Str(providerState.name), + providerType = TextReference.Str(providerState.type), + providerIcon = providerState.iconUrl, fee = TextReference.Str("${fee.amountCrypto} ${fee.symbolCrypto} (${fee.amountFiatFormatted})"), - rate = TextReference.Str("1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol"), - fromTokenAmount = TextReference.Str("${txState.fromAmount.orEmpty()} ${fromToken.tokenCurrency}}"), - toTokenAmount = TextReference.Str("${txState.toAmount.orEmpty()} ${toToken.tokenCurrency}}"), - fromTokenFiatAmount = TextReference.Str(fromToken.amountEquivalent.orEmpty()), - toTokenFiatAmount = TextReference.Str(toToken.amountEquivalent.orEmpty()), + rate = TextReference.Str(providerState.rate), + fromTokenAmount = TextReference.Str(txState.fromAmount.orEmpty()), + toTokenAmount = TextReference.Str(txState.toAmount.orEmpty()), + fromTokenFiatAmount = TextReference.Str(fromFiatAmount), + toTokenFiatAmount = TextReference.Str(toFiatAmount), fromTokenIconState = fromTokenIconState, toTokenIconState = toTokenIconState, onSecondaryButtonClick = onSecondaryBtnClick, @@ -892,7 +889,7 @@ internal class StateBuilder( return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) } - private fun getFormattedFiatAmount(amount: BigDecimal): String { + private fun getFormattedFiatAmount(amount: BigDecimal?): String { val appCurrency = appCurrencyProvider() return BigDecimalFormatter.formatFiatAmount(amount, appCurrency.code, appCurrency.symbol) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 909239af13..ffb9334830 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -23,7 +23,6 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType -import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.models.SwapSuccessStateHolder import com.tangem.feature.swap.presentation.R @@ -60,7 +59,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad .background(TangemTheme.colors.background.secondary) .padding(horizontal = TangemTheme.dimens.spacing16), ) { - TransactionDoneTitle(titleRes = R.string.swapping_success_view_title, date = 0L) + TransactionDoneTitle(titleRes = R.string.swapping_success_view_title, date = state.timestamp) SpacerH16() InputRowImage( title = TextReference.Res(R.string.swapping_success_from_title), @@ -83,9 +82,9 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad ) SpacerH16() InputRowBestRate( - imageUrl = state.selectedProvider.imageLarge, - title = TextReference.Str(state.selectedProvider.name), - titleExtra = TextReference.Str(state.selectedProvider.type.name), + imageUrl = state.providerIcon, + title = state.providerName, + titleExtra = state.providerType, subtitle = state.rate, modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) @@ -153,13 +152,9 @@ private val state = SwapSuccessStateHolder( timestamp = 0L, txUrl = "https://www.google.com/#q=nam", fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), - selectedProvider = SwapProvider( - providerId = "1inch", - rateTypes = listOf(), - name = "1inch", - type = ExchangeProviderType.DEX, - imageLarge = "", - ), + providerName = TextReference.Str("1inch"), + providerType = TextReference.Str(ExchangeProviderType.DEX.name), + providerIcon = "", fromTokenAmount = TextReference.Str("1 000 DAI"), toTokenAmount = TextReference.Str("1 000 MATIC"), fromTokenFiatAmount = TextReference.Str("1 000 $"), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index a577a98dfe..9c25f78b14 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -27,9 +27,7 @@ import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.Debouncer -import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* @@ -401,7 +399,8 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.createSuccessState( uiState = uiState, txState = it, - dataState = dataState, + fromAmount = dataState.amount?.toBigDecimal() ?: BigDecimal.ZERO, + toAmount = dataState.swapDataModel?.toTokenAmount?.value ?: BigDecimal.ZERO, txUrl = url, onSecondaryBtnClick = { val txHash = it.txAddress From d58ac113f5efb9dd323adbe1a8171317f0975e15 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Nov 2023 15:44:02 +0400 Subject: [PATCH 080/139] Updated on 2026-08-14 --- .../components/SettingsSwitchItem.kt | 2 +- .../bottomsheets/TangemBottomSheet.kt | 4 +- .../common/ui/ChooseWalletBottomSheet.kt | 22 +++ .../managetokens/state/ManageTokensState.kt | 24 +++ .../ManageTokensStatePreviewData.kt | 40 +++++ .../ui/ChooseNetworkBottomSheet.kt | 24 +++ .../managetokens/ui/ManageTokensScreen.kt | 144 ++++++++++++++++++ .../ui/components/AddCustomTokenButton.kt | 65 ++++++++ .../managetokens/ui/components/TokensList.kt | 59 +++++++ 9 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt create mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index b584136754..e4198a521c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -14,11 +14,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.SpacerW32 +import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item -import com.tangem.core.ui.components.TangemSwitch @Composable internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 2d9d85b164..76a172ffd9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color import com.tangem.core.ui.res.TangemTheme import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -20,6 +21,7 @@ import kotlinx.coroutines.launch @Composable inline fun TangemBottomSheet( config: TangemBottomSheetConfig, + contentColor: Color = TangemTheme.colors.background.primary, crossinline content: @Composable ColumnScope.(T) -> Unit, ) { var isVisible by remember { mutableStateOf(value = config.isShow) } @@ -29,7 +31,7 @@ inline fun TangemBottomSheet( ModalBottomSheet( onDismissRequest = config.onDismissRequest, sheetState = sheetState, - containerColor = TangemTheme.colors.background.primary, + containerColor = contentColor, shape = TangemTheme.shapes.bottomSheetLarge, dragHandle = { TangemBottomSheetDraggableHeader() }, ) { diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt new file mode 100644 index 0000000000..610f54edc4 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt @@ -0,0 +1,22 @@ +package com.tangem.managetokens.presentation.common.ui + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.managetokens.presentation.common.state.ChooseWalletState + +@Composable +internal fun ChooseWalletBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + contentColor = TangemTheme.colors.background.tertiary, + ) { + ChooseWalletScreen(state = it.chooseWalletState) + } +} + +internal class ChooseWalletBottomSheetConfig( + val chooseWalletState: ChooseWalletState.Choose, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt new file mode 100644 index 0000000000..1ee0ecebce --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt @@ -0,0 +1,24 @@ +package com.tangem.managetokens.presentation.managetokens.state + +import androidx.paging.PagingData +import com.tangem.core.ui.event.StateEvent +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.state.Event +import kotlinx.coroutines.flow.Flow + +internal data class ManageTokensState( + val searchBarState: SearchBarState, + val tokens: Flow>, + val isLoading: Boolean, + val addCustomTokenButton: AddCustomTokenButton, + val chooseWalletState: ChooseWalletState, + val derivationNotification: DerivationNotificationState? = null, + val selectedToken: TokenItemState.Loaded? = null, + val showChooseWalletScreen: Boolean = false, + val event: StateEvent, +) + +data class AddCustomTokenButton( + val isVisible: Boolean, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt new file mode 100644 index 0000000000..6ea8316ba5 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt @@ -0,0 +1,40 @@ +package com.tangem.managetokens.presentation.managetokens.state.previewdata + +import androidx.paging.PagingData +import com.tangem.core.ui.event.consumedEvent +import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData +import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton +import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState +import com.tangem.managetokens.presentation.managetokens.state.SearchBarState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import kotlinx.coroutines.flow.flowOf + +internal object ManageTokensStatePreviewData { + val loadedState: ManageTokensState + get() = ManageTokensState( + searchBarState = searchState, + tokens = flowOf(PagingData.from(tokens)), + isLoading = false, + addCustomTokenButton = AddCustomTokenButton(true, {}), + derivationNotification = DerivationNotificationStatePreviewData.state, + event = consumedEvent(), + chooseWalletState = ChooseWalletStatePreviewData.state, + ) + + val loadingState: ManageTokensState + get() = loadedState.copy(isLoading = true) + + private val tokens: List + get() = listOf( + TokenItemStatePreviewData.loadedPriceDown, + TokenItemStatePreviewData.loadedPriceUp, + ) + + private val searchState: SearchBarState + get() = SearchBarState( + query = "", + onQueryChange = {}, + active = false, + onActiveChange = {}, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt new file mode 100644 index 0000000000..cd1809a96b --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt @@ -0,0 +1,24 @@ +package com.tangem.managetokens.presentation.managetokens.ui + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState + +@Composable +internal fun ChooseNetworkBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + contentColor = TangemTheme.colors.background.tertiary, + ) { + ChooseNetworkScreen(state = it.selectedToken, walletState = it.chooseWalletState) + } +} + +internal class ChooseNetworkBottomSheetConfig( + val selectedToken: TokenItemState.Loaded, + val chooseWalletState: ChooseWalletState, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt new file mode 100644 index 0000000000..1bbf0c61a9 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt @@ -0,0 +1,144 @@ +package com.tangem.managetokens.presentation.managetokens.ui + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.Surface +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.managetokens.presentation.common.state.AlertState +import com.tangem.managetokens.presentation.common.state.ChooseWalletState +import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheet +import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheetConfig +import com.tangem.managetokens.presentation.common.ui.EventEffect +import com.tangem.managetokens.presentation.common.ui.components.Alert +import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState +import com.tangem.managetokens.presentation.managetokens.state.previewdata.ManageTokensStatePreviewData +import com.tangem.managetokens.presentation.managetokens.ui.components.DerivationNotification +import com.tangem.managetokens.presentation.managetokens.ui.components.TokensList +import com.tangem.managetokens.presentation.managetokens.ui.components.TokensSearchBar + +@Composable +internal fun ManageTokensScreen(state: ManageTokensState) { + var alertState by remember { mutableStateOf(value = null) } + + EventEffect( + event = state.event, + onAlertStateSet = { alertState = it }, + ) + alertState?.let { + Alert(state = it, onDismiss = { alertState = null }) + } + + Content(state) +} + +@Composable +private fun Content(state: ManageTokensState) { + Box( + modifier = Modifier + .fillMaxSize() + .background(color = TangemTheme.colors.background.primary) + .padding(top = TangemTheme.dimens.spacing32), + ) { + Column { + val listState = rememberLazyListState() + val raiseSearchBar by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } + + val elevation by animateDpAsState( + targetValue = if (raiseSearchBar) { + TangemTheme.dimens.elevation8 + } else { + TangemTheme.dimens.elevation0 + }, + label = "top_bar_elevation", + ) + Surface( + elevation = elevation, + modifier = Modifier, + ) { + TokensSearchBar( + state = state.searchBarState, + modifier = Modifier + .background(color = TangemTheme.colors.background.primary) + .padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing20), + ) + } + val tokens = state.tokens.collectAsLazyPagingItems() + TokensList(tokens = tokens, addCustomTokenButton = state.addCustomTokenButton) + } + state.derivationNotification?.let { + DerivationNotification( + config = it.config, + modifier = Modifier + .align(Alignment.BottomCenter), + ) + } + state.selectedToken?.let { selectedToken -> + ManageTokensBottomSheet(selectedToken = selectedToken, state = state) + } + } +} + +@Composable +private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state: ManageTokensState) { + if (state.showChooseWalletScreen && state.chooseWalletState is ChooseWalletState.Choose) { + val config = TangemBottomSheetConfig( + isShow = true, + content = ChooseWalletBottomSheetConfig(state.chooseWalletState), + onDismissRequest = state.chooseWalletState.onCloseChoosingWalletClick, + ) + ChooseWalletBottomSheet(config) + } else { + val config = TangemBottomSheetConfig( + isShow = true, + content = ChooseNetworkBottomSheetConfig( + selectedToken = selectedToken, + chooseWalletState = state.chooseWalletState, + ), + onDismissRequest = selectedToken.chooseNetworkState.onCloseChooseNetworkScreen, + ) + ChooseNetworkBottomSheet(config) + } +} + +@Preview +@Composable +private fun Preview_ManageTokensScreen_LightTheme( + @PreviewParameter(ManageTokensConfigProvider::class) + state: ManageTokensState, +) { + TangemTheme(isDark = false) { + ManageTokensScreen(state) + } +} + +@Preview +@Composable +private fun Preview_ManageTokensScreen_DarkTheme( + @PreviewParameter(ManageTokensConfigProvider::class) + state: ManageTokensState, +) { + TangemTheme(isDark = true) { + ManageTokensScreen(state) + } +} + +private class ManageTokensConfigProvider : CollectionPreviewParameterProvider( + collection = listOf( + ManageTokensStatePreviewData.loadingState, + ManageTokensStatePreviewData.loadedState, + ), +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt new file mode 100644 index 0000000000..195bb4d60c --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt @@ -0,0 +1,65 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R + +@Composable +internal fun AddCustomTokenButton(onButtonClick: () -> Unit, modifier: Modifier = Modifier) { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size68) + .fillMaxWidth() + .clickable { onButtonClick() } + .padding(horizontal = TangemTheme.dimens.spacing16), + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .background(color = TangemTheme.colors.button.secondary, shape = CircleShape), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_plus_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + SpacerW(width = TangemTheme.dimens.spacing12) + Text( + text = stringResource(id = R.string.add_custom_token_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Preview +@Composable +private fun AddCustomTokenButton_Preview_Light() { + TangemTheme(isDark = false) { + AddCustomTokenButton(onButtonClick = { }) + } +} + +@Preview +@Composable +private fun AddCustomTokenButton_Preview_Dark() { + TangemTheme(isDark = true) { + AddCustomTokenButton(onButtonClick = { }) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt new file mode 100644 index 0000000000..fe6c1c2bf7 --- /dev/null +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt @@ -0,0 +1,59 @@ +package com.tangem.managetokens.presentation.managetokens.ui.components + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.managetokens.impl.R +import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton +import com.tangem.managetokens.presentation.managetokens.state.TokenItemState + +private const val PLACEHOLDER_ITEMS_COUNT = 50 + +@Composable +internal fun TokensList(tokens: LazyPagingItems, addCustomTokenButton: AddCustomTokenButton) { + LazyColumn { + item { + Text( + text = stringResource(id = R.string.manage_tokens_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) + } + + if (tokens.loadState.refresh is LoadState.Loading) { + items(PLACEHOLDER_ITEMS_COUNT) { + TokenRowItem(state = TokenItemState.Loading(it.toString())) + } + } else { + val tokensList = tokens.itemSnapshotList + if (tokensList.isEmpty()) { + item { + Text( + text = stringResource(id = R.string.manage_tokens_nothing_found), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) + } + } + + items(items = tokensList.items, key = TokenItemState::id) { token -> + TokenRowItem(state = token) + } + + if (addCustomTokenButton.isVisible) { + item { + AddCustomTokenButton(onButtonClick = { addCustomTokenButton.onClick }) + } + } + } + } +} \ No newline at end of file From 6a5cb8f8d559916835c0c90f04c0e03b6a5eaeb0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 29 Nov 2023 12:10:09 +0300 Subject: [PATCH 081/139] Updated on 2026-08-14 --- .../api/express/TangemExpressApi.kt | 4 +- .../response/ExchangeResultsResponse.kt | 42 ----- .../models/response/ExchangeStatusResponse.kt | 59 +++++++ .../local/preferences/PreferencesKeys.kt | 2 + features/swap/data/build.gradle.kts | 10 +- .../swap/DefaultSwapTransactionRepository.kt | 163 ++++++++++++++++++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 23 +++ .../converters/ExchangeStatusConverter.kt | 19 ++ .../tangem/feature/swap/di/SwapDataModule.kt | 11 ++ features/swap/domain/build.gradle.kts | 3 + .../feature/swap/domain/SwapRepository.kt | 3 + .../swap/domain/SwapTransactionRepository.kt | 29 ++++ .../domain/models/domain/ExchangeStatus.kt | 2 +- .../domain/SavedSwapTransactionListModel.kt | 19 ++ 14 files changed, 341 insertions(+), 48 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeResultsResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt create mode 100644 features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index b56262221a..b5ef12459c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -49,6 +49,6 @@ interface TangemExpressApi { @Query("toAddress") toAddress: String, ): ApiResponse - @GET("exchange-result") - suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse + @GET("exchange-status") + suspend fun getExchangeStatus(@Query("txId") txId: String): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeResultsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeResultsResponse.kt deleted file mode 100644 index c9b10f881e..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeResultsResponse.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.tangem.datasource.api.express.models.response - -import com.squareup.moshi.Json - -data class ExchangeResultsResponse( - @Json(name = "status") - val status: ExchangeResultsStatus, - - @Json(name = "externalStatus") - val externalStatus: String, - - @Json(name = "externalTxUrl") - val externalTxUrl: String, - - @Json(name = "error") - val error: ExchangeResultsError?, -) - -enum class ExchangeResultsStatus { - @Json(name = "processing") - PROCESSING, - - @Json(name = "done") - DONE, - - @Json(name = "failed") - FAILED, - - @Json(name = "refunded") - REFUNDED, - - @Json(name = "verificationRequired") - VERIFICATION_REQUIRED, -} - -data class ExchangeResultsError( - @Json(name = "code") - val code: Int, - - @Json(name = "description") - val description: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt new file mode 100644 index 0000000000..94cd9cbf1f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt @@ -0,0 +1,59 @@ +package com.tangem.datasource.api.express.models.response + +import com.squareup.moshi.Json + +data class ExchangeStatusResponse( + + @Json(name = "providerId") + val providerId: String, + + @Json(name = "externalTxId") + val externalTxId: String, + + @Json(name = "externalTxStatus") + val externalStatus: ExchangeStatus, + + @Json(name = "externalTxUrl") + val externalTxUrl: String, + + @Json(name = "error") + val error: ExchangeStatusError?, +) + +enum class ExchangeStatus { + + @Json(name = "new") + NEW, + + @Json(name = "waiting") + WAITING, + + @Json(name = "confirming") + CONFIRMING, + + @Json(name = "exchanging") + EXCHANGING, + + @Json(name = "sending") + SENDING, + + @Json(name = "finished") + FINISHED, + + @Json(name = "failed") + FAILED, + + @Json(name = "refunded") + REFUNDED, + + @Json(name = "verifying") + VERIFYING, +} + +data class ExchangeStatusError( + @Json(name = "code") + val code: Int, + + @Json(name = "description") + val description: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index d81065c147..7f43d0a7e8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -32,6 +32,8 @@ object PreferencesKeys { val SELECTED_APP_CURRENCY_KEY by lazy { stringPreferencesKey(name = "selectedAppCurrency") } val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") } + + val SWAP_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "swapTransactions") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 2d753c6fa2..53dddfc5a9 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -8,15 +8,19 @@ plugins { dependencies { + /** AndroidX */ + implementation(deps.androidx.datastore) + /** Project*/ - implementation(project(":core:datasource")) - implementation(project(":core:utils")) - implementation(project(":features:swap:domain")) + implementation(projects.core.datasource) + implementation(projects.core.utils) + implementation(projects.features.swap.domain) /** Network */ implementation(deps.retrofit) implementation(deps.moshi) implementation(deps.moshi.kotlin) + implementation(deps.arrow.core) /** Domain */ implementation(projects.domain.tokens.models) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt new file mode 100644 index 0000000000..63c8aecb72 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -0,0 +1,163 @@ +package com.tangem.feature.swap + +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectList +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.swap.domain.SwapTransactionRepository +import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel +import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class DefaultSwapTransactionRepository( + private val appPreferencesStore: AppPreferencesStore, +) : SwapTransactionRepository { + + override suspend fun storeTransaction( + userWalletId: UserWalletId, + fromCryptoCurrencyId: CryptoCurrency.ID, + toCryptoCurrencyId: CryptoCurrency.ID, + transaction: SavedSwapTransactionModel, + ) { + appPreferencesStore.editData { mutablePreferences -> + val savedTransactions: List? = mutablePreferences.getObjectList( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + ) + + val tokenTransactions = savedTransactions + ?.firstOrNull { + it.checkId( + checkUserWalletId = userWalletId, + fromCurrencyId = fromCryptoCurrencyId, + toCurrencyId = toCryptoCurrencyId, + ) + } + ?.transactions + ?.addOrReplace( + item = transaction, + predicate = { it.txId == transaction.txId }, + ) ?: listOf(transaction) + + mutablePreferences.setObject( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + value = savedTransactions?.updateList( + userWalletId = userWalletId, + fromCryptoCurrencyId = fromCryptoCurrencyId, + toCryptoCurrencyId = toCryptoCurrencyId, + transactions = tokenTransactions, + ) ?: listOf( + SavedSwapTransactionListModel( + userWalletId = userWalletId.stringValue, + fromCryptoCurrencyId = fromCryptoCurrencyId.value, + toCryptoCurrencyId = toCryptoCurrencyId.value, + transactions = tokenTransactions, + ), + ), + ) + } + } + + override fun getTransactions( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + ): Flow?> { + return appPreferencesStore.getObjectList( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + ).map { savedTransactions -> + savedTransactions + ?.filter { + it.userWalletId == userWalletId.stringValue && + ( + it.toCryptoCurrencyId == cryptoCurrencyId.value || + it.fromCryptoCurrencyId == cryptoCurrencyId.value + ) + } + } + } + + override suspend fun removeTransaction( + userWalletId: UserWalletId, + fromCryptoCurrencyId: CryptoCurrency.ID, + toCryptoCurrencyId: CryptoCurrency.ID, + txId: String, + ) { + appPreferencesStore.editData { mutablePreferences -> + val savedList: List? = mutablePreferences.getObjectList( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + ) + val tokenTransactions = savedList + ?.first { + it.checkId( + checkUserWalletId = userWalletId, + fromCurrencyId = fromCryptoCurrencyId, + toCurrencyId = toCryptoCurrencyId, + ) + } + ?.transactions + ?.dropWhile { it.txId == txId } + + val editedList = + if (tokenTransactions.isNullOrEmpty()) { + savedList?.dropWhile { + it.checkId( + checkUserWalletId = userWalletId, + fromCurrencyId = fromCryptoCurrencyId, + toCurrencyId = toCryptoCurrencyId, + ) + } + } else { + savedList.updateList( + userWalletId = userWalletId, + fromCryptoCurrencyId = fromCryptoCurrencyId, + toCryptoCurrencyId = toCryptoCurrencyId, + transactions = tokenTransactions, + ) + } + + if (editedList.isNullOrEmpty()) { + mutablePreferences.remove(key = PreferencesKeys.SWAP_TRANSACTIONS_KEY) + } else { + mutablePreferences.setObject( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + value = editedList, + ) + } + } + } + + private fun SavedSwapTransactionListModel.checkId( + checkUserWalletId: UserWalletId, + fromCurrencyId: CryptoCurrency.ID, + toCurrencyId: CryptoCurrency.ID, + ): Boolean { + return userWalletId == checkUserWalletId.stringValue && + toCryptoCurrencyId == toCurrencyId.value && + fromCryptoCurrencyId == fromCurrencyId.value + } + + private fun List.updateList( + userWalletId: UserWalletId, + fromCryptoCurrencyId: CryptoCurrency.ID, + toCryptoCurrencyId: CryptoCurrency.ID, + transactions: List, + ): List { + return addOrReplace( + item = SavedSwapTransactionListModel( + userWalletId = userWalletId.stringValue, + fromCryptoCurrencyId = fromCryptoCurrencyId.value, + toCryptoCurrencyId = toCryptoCurrencyId.value, + transactions = transactions, + ), + predicate = { + it.checkId( + checkUserWalletId = userWalletId, + fromCurrencyId = fromCryptoCurrencyId, + toCurrencyId = toCryptoCurrencyId, + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 9ea783fc6b..338fafe196 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -1,5 +1,8 @@ package com.tangem.feature.swap +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Approver import com.tangem.blockchain.common.Blockchain @@ -52,6 +55,7 @@ internal class SwapRepositoryImpl @Inject constructor( private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() private val cryptoCurrencyFactory = CryptoCurrencyFactory() + private val exchangeStatusConverter = ExchangeStatusConverter() override suspend fun getPairs( initialCurrency: LeastTokenInfo, @@ -103,6 +107,25 @@ internal class SwapRepositoryImpl @Inject constructor( ).getOrThrow() } + override suspend fun getExchangeStatus(txId: String): Either { + return withContext(coroutineDispatcher.io) { + either { + catch( + { + exchangeStatusConverter.convert( + tangemExpressApi + .getExchangeStatus(txId) + .getOrThrow(), + ) + }, + { + raise(UnknownError(it.message)) + }, + ) + } + } + } + override suspend fun getRates(currencyId: String, tokenIds: List): Map { // workaround cause backend do not return arbitrum and optimism rates val addedTokens = if (tokenIds.contains(OPTIMISM_ID) || tokenIds.contains(ARBITRUM_ID)) { diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt new file mode 100644 index 0000000000..f5901e8c69 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.express.models.response.ExchangeStatusResponse +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel +import com.tangem.utils.converter.Converter + +internal class ExchangeStatusConverter : Converter { + override fun convert(value: ExchangeStatusResponse): ExchangeStatusModel { + return ExchangeStatusModel( + providerId = value.providerId, + status = ExchangeStatus.values().firstOrNull { + it.name.lowercase() == value.externalStatus.name.lowercase() + }, + txId = value.externalTxId, + txUrl = value.externalTxUrl, + ) + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 4c34db7b17..b30b20f817 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -7,11 +7,14 @@ import com.tangem.datasource.api.oneinch.OneInchApiFactory import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.feature.swap.SwapRepositoryImpl import com.tangem.feature.swap.converters.ErrorsDataConverter +import com.tangem.feature.swap.DefaultSwapTransactionRepository import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -47,6 +50,14 @@ internal class SwapDataModule { ) } + @Provides + @Singleton + fun provideSwapTransactionRepository(appPreferencesStore: AppPreferencesStore): SwapTransactionRepository { + return DefaultSwapTransactionRepository( + appPreferencesStore = appPreferencesStore, + ) + } + @Provides @Singleton internal fun provideErrorsConverter(@NetworkMoshi moshi: Moshi): ErrorsDataConverter { diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 4f4d42fe13..f3841b400d 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -27,9 +27,11 @@ dependencies { implementation(projects.domain.legacy) implementation(projects.domain.demo) implementation(projects.domain.card) + implementation(projects.domain.appCurrency.models) /** Core modules */ implementation(projects.core.utils) + implementation(projects.core.ui) /** Feature Apis */ implementation(projects.features.wallet.api) @@ -40,4 +42,5 @@ dependencies { implementation(deps.arrow.core) implementation(deps.timber) implementation(deps.tangem.blockchain) + implementation(deps.moshi) } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 13650fe4fe..c96bb13b19 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.domain +import arrow.core.Either import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel @@ -14,6 +15,8 @@ interface SwapRepository { suspend fun getExchangeableTokens(networkId: String): List + suspend fun getExchangeStatus(txId: String): Either + @Suppress("LongParameterList") suspend fun findBestQuote( fromContractAddress: String, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt new file mode 100644 index 0000000000..d89914d537 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.swap.domain + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel +import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel +import kotlinx.coroutines.flow.Flow + +interface SwapTransactionRepository { + + suspend fun storeTransaction( + userWalletId: UserWalletId, + fromCryptoCurrencyId: CryptoCurrency.ID, + toCryptoCurrencyId: CryptoCurrency.ID, + transaction: SavedSwapTransactionModel, + ) + + fun getTransactions( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + ): Flow?> + + suspend fun removeTransaction( + userWalletId: UserWalletId, + fromCryptoCurrencyId: CryptoCurrency.ID, + toCryptoCurrencyId: CryptoCurrency.ID, + txId: String, + ) +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt index a74e8fbb6a..dc5c5738be 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain.models.domain data class ExchangeStatusModel( - val providerId: Int, + val providerId: String, val status: ExchangeStatus? = null, val txId: String? = null, val txUrl: String? = null, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt new file mode 100644 index 0000000000..6b59b50197 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.swap.domain.models.domain + +data class SavedSwapTransactionListModel( + val userWalletId: String, + val fromCryptoCurrencyId: String, + val toCryptoCurrencyId: String, + val transactions: List, +) + +data class SavedSwapTransactionModel( + val txId: String, + val timestamp: Long, + val fromCryptoAmount: String, + val toCryptoAmount: String, + val provider: SwapProvider, + val toFiatAmount: String? = null, + val fromFiatAmount: String? = null, + val status: ExchangeStatusModel? = null, +) \ No newline at end of file From f0886b4b74b6feb5f592049d7d64554088db1f99 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 29 Nov 2023 16:07:00 +0300 Subject: [PATCH 082/139] Updated on 2026-08-14 --- .../core/ui/components/MiddleEllipsisText.kt | 39 +-- .../ui/components/atoms/text/BoundCounter.kt | 38 +++ .../ui/components/atoms/text/EllipsisText.kt | 242 ++++++++++++++++++ .../ui/components/inputrow/InputRowApprox.kt | 12 +- .../tokens/repository/MockQuotesRepository.kt | 5 + .../tokendetails/TokenDetailsPreviewData.kt | 1 + .../state/SwapTransactionsState.kt | 13 +- .../tokendetails/state/TokenDetailsState.kt | 1 + .../components/ExchangeStatusNotifications.kt | 38 +++ .../TokenDetailsSkeletonStateConverter.kt | 1 + .../tokendetails/ui/TokenDetailsScreen.kt | 15 +- .../exchange/ExchangeStatusBlock.kt | 38 ++- .../exchange/ExchangeStatusBottomSheet.kt | 20 +- .../exchange/ExchangeStatusItems.kt | 21 +- 14 files changed, 425 insertions(+), 59 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/MiddleEllipsisText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/MiddleEllipsisText.kt index 9bdff5f987..e67fbf3de2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/MiddleEllipsisText.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/MiddleEllipsisText.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.text.TextLayoutResult @@ -18,10 +17,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.TextUnit +import com.tangem.core.ui.components.atoms.text.BoundCounter /** * https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose */ +@Deprecated("Use EllipsisText with TextEllipsis.Middle ellipsis instead") @Suppress("LongMethod") @Composable fun MiddleEllipsisText( @@ -138,38 +139,4 @@ fun MiddleEllipsisText( private const val ELLIPSIS_CHARACTERS_COUNT = 3 private const val ELLIPSIS_CHARACTER = '.' -private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "") - -private class BoundCounter( - private val text: String, - private val textLayoutResult: TextLayoutResult, - private val charPosition: (Int) -> Int, -) { - var string = "" - private set - var width = 0f - private set - - private var _nextCharWidth: Float? = null - private var invalidCharsCount = 0 - - fun widthWithNextChar(): Float = width + nextCharWidth() - - private fun nextCharWidth(): Float = _nextCharWidth ?: run { - var boundingBox: Rect - // invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630 - invalidCharsCount-- - do { - boundingBox = textLayoutResult - .getBoundingBox(charPosition(string.count() + ++invalidCharsCount)) - } while (boundingBox.right == 0f) - _nextCharWidth = boundingBox.width - boundingBox.width - } - - fun addNextChar() { - string += text[charPosition(string.count())] - width += nextCharWidth() - _nextCharWidth = null - } -} \ No newline at end of file +private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "") \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt new file mode 100644 index 0000000000..6a97038b3d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/BoundCounter.kt @@ -0,0 +1,38 @@ +package com.tangem.core.ui.components.atoms.text + +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.text.TextLayoutResult + +internal class BoundCounter( + private val text: String, + private val textLayoutResult: TextLayoutResult, + private val charPosition: (Int) -> Int, +) { + var string = "" + private set + var width = 0f + private set + + private var _nextCharWidth: Float? = null + private var invalidCharsCount = 0 + + fun widthWithNextChar(): Float = width + nextCharWidth() + + private fun nextCharWidth(): Float = _nextCharWidth ?: run { + var boundingBox: Rect + // invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630 + invalidCharsCount-- + do { + boundingBox = textLayoutResult + .getBoundingBox(charPosition(string.count() + ++invalidCharsCount)) + } while (boundingBox.right == 0f) + _nextCharWidth = boundingBox.width + boundingBox.width + } + + fun addNextChar() { + string += text[charPosition(string.count())] + width += nextCharWidth() + _nextCharWidth = null + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt new file mode 100644 index 0000000000..d95b813ee1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt @@ -0,0 +1,242 @@ +package com.tangem.core.ui.components.atoms.text + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.TextUnit +import com.tangem.core.ui.res.TangemTheme + +sealed class TextEllipsis { + + object Middle : TextEllipsis() + + object End : TextEllipsis() + + data class OffsetEnd( + val offsetEnd: Int = 0, + val hasSeparator: Boolean = true, + ) : TextEllipsis() +} + +/** + * https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose + * + * Customized Text with ellipsis. Ellipsis can be placed in: Middle, End or OffsetEnd (OffsetEnd with separator). + * + * * OffsetEnd can be useful to display big amounts with currency symbol. OffsetEnd 0 is equal to End. + */ +@Suppress("LongMethod") +@Composable +fun EllipsisText( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + softWrap: Boolean = true, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + ellipsis: TextEllipsis = TextEllipsis.End, +) { + val ellipsisText = remember(text) { + if (ellipsis is TextEllipsis.OffsetEnd && ellipsis.hasSeparator) { + ELLIPSIS_TEXT_WITH_SEPARATOR + } else { + ELLIPSIS_TEXT + } + } + + // some letters, like "r", will have less width when placed right before "." + // adding a space to prevent such case + val layoutText = remember(text) { "$text $ellipsisText" } + val textLayoutResultState = remember(layoutText) { + mutableStateOf(null) + } + SubcomposeLayout(modifier) { constraints -> + // result is ignored - we only need to fill our textLayoutResult + subcompose("measure") { + Text( + text = layoutText, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + softWrap = softWrap, + maxLines = 1, + onTextLayout = { textLayoutResultState.value = it }, + style = style, + ) + }.first().measure(Constraints()) + // to allow smart cast + val textLayoutResult = textLayoutResultState.value + ?: // shouldn't happen - onTextLayout is called before subcompose finishes + return@SubcomposeLayout layout(0, 0) {} + val placeable = subcompose("visible") { + val finalText = remember(text, textLayoutResult, constraints.maxWidth) { + if ( + text.isEmpty() || + textLayoutResult.getBoundingBox(text.indices.last).right <= constraints.maxWidth + ) { + // text not including ellipsis fits on the first line. + return@remember text + } + + var ellipsisWidth = 0f + layoutText.indices.toList() + .takeLast(ellipsisText.length) + .forEach widthLet@{ + ellipsisWidth += textLayoutResult.getBoundingBox(it).width + } + + val availableWidth = constraints.maxWidth - ellipsisWidth + val startCounter = BoundCounter(text, textLayoutResult) { it } + val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it } + + when (ellipsis) { + TextEllipsis.Middle -> { + middleEllipsisText( + availableWidth, + startCounter, + endCounter, + ) + } + TextEllipsis.End -> { + offsetEndEllipsisText( + availableWidth = availableWidth, + startCounter = startCounter, + endCounter = endCounter, + ) + } + is TextEllipsis.OffsetEnd -> { + offsetEndEllipsisText( + availableWidth = availableWidth, + startCounter = startCounter, + endCounter = endCounter, + offsetEnd = ellipsis.offsetEnd, + withSeparator = ellipsis.hasSeparator, + ) + } + } + } + Text( + text = finalText, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + softWrap = softWrap, + onTextLayout = onTextLayout, + style = style, + ) + }[0].measure(constraints) + layout(placeable.width, placeable.height) { + placeable.place(0, 0) + } + } +} + +private const val ELLIPSIS_SEPARATOR = " " +private const val ELLIPSIS_TEXT = "..." +private const val ELLIPSIS_TEXT_WITH_SEPARATOR = ELLIPSIS_TEXT.plus(ELLIPSIS_SEPARATOR) + +private fun middleEllipsisText(availableWidth: Float, startCounter: BoundCounter, endCounter: BoundCounter): String { + while (availableWidth - startCounter.width - endCounter.width > 0) { + val possibleEndWidth = endCounter.widthWithNextChar() + if ( + startCounter.width >= possibleEndWidth && + availableWidth - startCounter.width - possibleEndWidth >= 0 + ) { + endCounter.addNextChar() + } else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) { + startCounter.addNextChar() + } else { + break + } + } + return startCounter.string.trimEnd() + ELLIPSIS_TEXT + endCounter.string.reversed().trimStart() +} + +private fun offsetEndEllipsisText( + availableWidth: Float, + startCounter: BoundCounter, + endCounter: BoundCounter, + offsetEnd: Int = 0, + withSeparator: Boolean = false, +): String { + while (availableWidth - startCounter.width - endCounter.width > 0) { + val possibleEndWidth = endCounter.widthWithNextChar() + if ( + offsetEnd > endCounter.string.length && + availableWidth - startCounter.width - possibleEndWidth >= 0 + ) { + endCounter.addNextChar() + } else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) { + startCounter.addNextChar() + } else { + break + } + } + val ellipsis = if (withSeparator) ELLIPSIS_TEXT_WITH_SEPARATOR else ELLIPSIS_TEXT + + return startCounter.string.trimEnd() + ellipsis + endCounter.string.reversed().trimStart() +} + +//region Preview +@Preview(widthDp = 200) +@Composable +private fun EllipsisTexPreview(@PreviewParameter(EllipsisTexPreviewParameterProvider::class) ellipsis: TextEllipsis) { + TangemTheme { + EllipsisText( + text = "11111111111111111111111111111111111111111111111111 END", + ellipsis = ellipsis, + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .fillMaxWidth(), + ) + } +} + +private class EllipsisTexPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + TextEllipsis.Middle, + TextEllipsis.End, + TextEllipsis.OffsetEnd("TEXT".length), + TextEllipsis.OffsetEnd("TEXT".length, false), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt index 3b085833a3..bd69b6feb9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt @@ -3,13 +3,13 @@ package com.tangem.core.ui.components.inputrow import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.components.inputrow.inner.DividerContainer @@ -56,6 +56,7 @@ fun InputRowApprox( iconState = leftIcon, title = leftTitle, subtitle = leftSubtitle, + modifier = Modifier.weight(1f), ) Icon( painter = painterResource(id = R.drawable.ic_approx_24), @@ -71,6 +72,7 @@ fun InputRowApprox( iconState = rightIcon, title = rightTitle, subtitle = rightSubtitle, + modifier = Modifier.weight(1f), ) } } @@ -97,12 +99,12 @@ private fun InputRowApproxItem( start = TangemTheme.dimens.spacing12, ), ) { - Text( + EllipsisText( text = title.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, ) - Text( + EllipsisText( text = subtitle.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, @@ -123,7 +125,7 @@ private fun InputRowApproxPreview_Light() { leftTitle = TextReference.Str("Left title"), leftSubtitle = TextReference.Str("Left subtitle"), rightIcon = TokenIconState.Loading, - rightTitle = TextReference.Str("Right title"), + rightTitle = TextReference.Str("Right title Right title Right title Right title Right title"), rightSubtitle = TextReference.Str("Right subtitle"), modifier = Modifier .background(TangemTheme.colors.background.action), @@ -137,7 +139,7 @@ private fun InputRowApproxPreview_Dark() { TangemTheme(isDark = true) { InputRowApprox( leftIcon = TokenIconState.Loading, - leftTitle = TextReference.Str("Left title"), + leftTitle = TextReference.Str("Left title Left title Left title Left title Left title"), leftSubtitle = TextReference.Str("Left subtitle"), rightIcon = TokenIconState.Loading, rightTitle = TextReference.Str("Right title"), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index 6d1c1e1a27..ae960f191a 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -20,4 +20,9 @@ internal class MockQuotesRepository( override suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set { return getQuotesUpdates(currenciesIds).first() } + + override suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote { + return quotes.map { it.getOrElse { e -> throw e } }.first() + .first { it.rawCurrencyId == currencyId.rawCurrencyId } + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 2a25900715..458e547586 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -101,6 +101,7 @@ internal object TokenDetailsPreviewData { ), dialogConfig = null, pendingTxs = persistentListOf(), + swapTxs = persistentListOf(), pullToRefreshConfig = pullToRefreshConfig, bottomSheetConfig = null, isBalanceHidden = false, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index 8b4fb5c8a5..9941c1bbda 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -2,21 +2,25 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications import kotlinx.collections.immutable.PersistentList -import java.math.BigDecimal internal data class SwapTransactionsState( val txId: String, - val providerId: Int, + val provider: SwapProvider, val txUrl: String? = null, - val rate: BigDecimal, val timestamp: Long, - val status: PersistentList, + val statuses: PersistentList, val activeStatus: ExchangeStatus?, + val fiatSymbol: String, + val notification: ExchangeStatusNotifications? = null, val toCryptoAmount: String, + val toCryptoSymbol: String, val toFiatAmount: String, val toCurrencyIcon: TokenIconState, val fromCryptoAmount: String, + val fromCryptoSymbol: String, val fromFiatAmount: String, val fromCurrencyIcon: TokenIconState, val onClick: () -> Unit, @@ -25,7 +29,6 @@ internal data class SwapTransactionsState( internal class ExchangeStatusState( val status: ExchangeStatus, - val text: String, val isActive: Boolean, val isDone: Boolean, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 15f290adde..f9f5dfcabd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -19,6 +19,7 @@ internal data class TokenDetailsState( val marketPriceBlockState: MarketPriceBlockState, val notifications: ImmutableList, val pendingTxs: PersistentList, + val swapTxs: PersistentList, val txHistoryState: TxHistoryState, val dialogConfig: TokenDetailsDialogConfig?, val pullToRefreshConfig: TokenDetailsPullToRefreshConfig, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt new file mode 100644 index 0000000000..1da8fd0672 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/ExchangeStatusNotifications.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.components + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.tokendetails.impl.R + +@Immutable +internal sealed class ExchangeStatusNotifications(val config: NotificationConfig) { + + data class NeedVerification( + val onGoToProviderClick: () -> Unit, + ) : ExchangeStatusNotifications( + config = NotificationConfig( + title = TextReference.Res(R.string.express_exchange_notification_verification_title), + subtitle = TextReference.Res(R.string.express_exchange_notification_verification_text), + iconResId = R.drawable.ic_alert_triangle_20, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = TextReference.Res(R.string.express_go_to_provider), + onClick = onGoToProviderClick, + ), + ), + ) + + data class Failed( + val onGoToProviderClick: () -> Unit, + ) : ExchangeStatusNotifications( + config = NotificationConfig( + title = TextReference.Res(R.string.express_exchange_notification_failed_title), + subtitle = TextReference.Res(R.string.express_exchange_notification_failed_text), + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = TextReference.Res(R.string.express_go_to_provider), + onClick = onGoToProviderClick, + ), + ), + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index e92142e90c..30d410450e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -45,6 +45,7 @@ internal class TokenDetailsSkeletonStateConverter( marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol), notifications = persistentListOf(), pendingTxs = persistentListOf(), + swapTxs = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 9b4e73ef9a..57f5e40688 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -2,7 +2,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.ExperimentalMaterialApi @@ -40,6 +42,9 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheet +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.swapTransactionsItems // TODO: Split to blocks [REDACTED_JIRA] @Suppress("LongMethod") @@ -117,6 +122,11 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) } + swapTransactionsItems( + state.swapTxs, + itemModifier, + ) + txHistoryItems( state = state.txHistoryState, isBalanceHidden = state.isBalanceHidden, @@ -141,6 +151,9 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { is ChooseAddressBottomSheetConfig -> { ChooseAddressBottomSheet(config = config) } + is ExchangeStatusBottomSheetConfig -> { + ExchangeStatusBottomSheet(config = config) + } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index a583d1cb56..b9d7dd77ee 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -26,7 +26,7 @@ import kotlinx.collections.immutable.PersistentList @Composable internal fun ExchangeStatusBlock( - status: PersistentList, + statuses: PersistentList, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -70,10 +70,10 @@ internal fun ExchangeStatusBlock( } } - status.forEachIndexed { index, item -> + statuses.forEachIndexed { index, item -> ExchangeStatusStep( stepStatus = item, - isLast = index == status.lastIndex, + isLast = index == statuses.lastIndex, ) } } @@ -128,7 +128,7 @@ private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) { } Text( - text = stepStatus.text, + text = getStatusText(stepStatus)?.let { stringResource(it) }.orEmpty(), style = TangemTheme.typography.body2, color = textColor, modifier = Modifier @@ -206,4 +206,34 @@ private fun ExchangeStepSeparator() { shape = CircleShape, ), ) +} + +private fun getStatusText(stepStatus: ExchangeStatusState) = when (stepStatus.status) { + ExchangeStatus.Failed -> R.string.express_exchange_status_failed + ExchangeStatus.Verifying -> if (stepStatus.isDone) { + R.string.express_exchange_status_verified + } else { + R.string.express_exchange_status_verifying + } + ExchangeStatus.New, ExchangeStatus.Waiting -> if (stepStatus.isDone) { + R.string.express_exchange_status_received + } else { + R.string.express_exchange_status_receiving + } + ExchangeStatus.Confirming -> if (stepStatus.isDone) { + R.string.express_exchange_status_confirmed + } else { + R.string.express_exchange_status_confirming + } + ExchangeStatus.Exchanging -> if (stepStatus.isDone) { + R.string.express_exchange_status_exchanged + } else { + R.string.express_exchange_status_exchanging + } + ExchangeStatus.Sending -> if (stepStatus.isDone) { + R.string.express_exchange_status_sent + } else { + R.string.express_exchange_status_sending + } + else -> null } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index 5705f00a63..b32c69f48e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -15,6 +16,7 @@ import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.toDateFormat @@ -64,17 +66,25 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC toFiatAmount = TextReference.Str(config.toFiatAmount), ) SpacerH12() - // todo replace with real provider data ExchangeProvider( - providerName = TextReference.Str(config.providerId.toString()), - providerType = TextReference.Str("CEX"), - imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/changenow_512.png", + providerName = TextReference.Str(config.provider.name), + providerType = TextReference.Str(config.provider.type.name), + imageUrl = config.provider.imageLarge, ) SpacerH12() ExchangeStatusBlock( - status = config.status, + statuses = config.statuses, onClick = config.onGoToProviderClick, ) + AnimatedContent( + targetState = config.notification, + label = "Exchange Status Notification Change", + ) { + it?.let { + SpacerH12() + Notification(config = it.config) + } + } SpacerH24() } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index 95ac42480e..4ea9a31b08 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -17,7 +17,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension import androidx.constraintlayout.compose.Visibility +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.res.TangemTheme @@ -46,11 +49,13 @@ internal fun LazyListScope.swapTransactionsItems( } ExchangeStatusItem( - providerName = item.providerId.toString(), + providerName = item.provider.name, fromTokenIconState = item.fromCurrencyIcon, toTokenIconState = item.toCurrencyIcon, fromAmount = item.fromCryptoAmount, + fromSymbol = item.fromCryptoSymbol, toAmount = item.toCryptoAmount, + toSymbol = item.toCryptoSymbol, onClick = item.onClick, infoIconRes = iconRes, infoIconTint = tint, @@ -67,7 +72,9 @@ private fun ExchangeStatusItem( fromTokenIconState: TokenIconState, toTokenIconState: TokenIconState, fromAmount: String, + fromSymbol: String, toAmount: String, + toSymbol: String, onClick: () -> Unit, modifier: Modifier = Modifier, @DrawableRes infoIconRes: Int? = null, @@ -104,14 +111,17 @@ private fun ExchangeStatusItem( bottom.linkTo(parent.bottom) }, ) - Text( + EllipsisText( text = fromAmount, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + ellipsis = TextEllipsis.OffsetEnd(fromSymbol.length), modifier = Modifier.constrainAs(fromRef) { start.linkTo(fromIconRef.end, padding6) top.linkTo(titleRef.bottom, padding6) + end.linkTo(swapIconRef.start) bottom.linkTo(parent.bottom) + width = Dimension.fillToConstraints }, ) Icon( @@ -123,6 +133,7 @@ private fun ExchangeStatusItem( .constrainAs(swapIconRef) { start.linkTo(fromRef.end, padding6) top.linkTo(titleRef.bottom, padding6) + end.linkTo(toIconRef.start) bottom.linkTo(parent.bottom) }, ) @@ -134,17 +145,21 @@ private fun ExchangeStatusItem( .constrainAs(toIconRef) { start.linkTo(swapIconRef.end, padding6) top.linkTo(titleRef.bottom, padding6) + end.linkTo(toRef.start) bottom.linkTo(parent.bottom) }, ) - Text( + EllipsisText( text = toAmount, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + ellipsis = TextEllipsis.OffsetEnd(toSymbol.length), modifier = Modifier.constrainAs(toRef) { start.linkTo(toIconRef.end, padding6) top.linkTo(titleRef.bottom, padding6) + end.linkTo(infoIconRef.start, padding6) bottom.linkTo(parent.bottom) + width = Dimension.fillToConstraints }, ) Icon( From 62f7c337ff8cabb9b0e0384b1ed87d1c1dc63f71 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 29 Nov 2023 18:45:53 +0300 Subject: [PATCH 083/139] Updated on 2026-08-14 --- .../models/response/ExpressErrorResponse.kt | 2 +- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../swap/converters/ErrorsDataConverter.kt | 4 +- .../feature/swap/domain/SwapInteractorImpl.kt | 42 +++-- .../swap/domain/models/ui/SwapState.kt | 5 +- .../feature/swap/models/SwapStateHolder.kt | 1 + .../tangem/feature/swap/ui/StateBuilder.kt | 146 +++++++++++++++--- .../feature/swap/ui/SwapSelectTokenScreen.kt | 4 +- .../feature/swap/viewmodels/SwapViewModel.kt | 10 +- 10 files changed, 174 insertions(+), 42 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt index c0c39b0fb2..ea8a552360 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressErrorResponse.kt @@ -21,7 +21,7 @@ data class ExpressError( data class ExpressErrorValue( @Json(name = "minAmount") - val minAmount: BigDecimal?, + val minAmount: String?, @Json(name = "decimals") val decimals: Int?, diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 194da3dfb2..8805ebe16d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -201,6 +201,7 @@ Операция не выполнена провайдером Посетите сайт провайдера для проверки Провайдер: требуется верификация + Список токенов в вашем кошельке Получение наилучших курсов... Провайдер Лучший курс diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8d4ff0b882..be887dc4ea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -215,6 +215,7 @@ Exchange status Verified Verification required + List of all tokens added to your wallet Fetching best rates... Floating rate Go to provider diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index 5330219b1e..2068fc982a 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -3,7 +3,7 @@ package com.tangem.feature.swap.converters import com.squareup.moshi.JsonAdapter import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.feature.swap.domain.models.DataError -import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.utils.converter.Converter internal class ErrorsDataConverter( @@ -23,7 +23,7 @@ internal class ErrorsDataConverter( 2240 -> DataError.ExchangeNotPossibleError(code = error.code) 2250 -> DataError.ExchangeTooSmallAmountError( code = error.code, - amount = SwapAmount( + amount = createFromAmountWithOffset( requireNotNull(error.value?.minAmount), requireNotNull(error.value?.decimals), ), diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index da5dd7b595..afc2b3787b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -501,14 +501,16 @@ internal class SwapInteractorImpl @Inject constructor( network = currencyToSend.currency.network, ) - return result.fold(ifLeft = { - when (it) { - is SendTransactionError.NetworkError -> TxState.NetworkError - is SendTransactionError.DataError -> TxState.BlockchainError - SendTransactionError.DemoCardError -> TxState.UnknownError - else -> TxState.UnknownError - } - }, ifRight = { + return result.fold( + ifLeft = { + when (it) { + is SendTransactionError.NetworkError -> TxState.NetworkError + is SendTransactionError.DataError -> TxState.BlockchainError + SendTransactionError.DemoCardError -> TxState.UnknownError + else -> TxState.UnknownError + } + }, + ifRight = { TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, @@ -523,7 +525,8 @@ internal class SwapInteractorImpl @Inject constructor( derivationPath, ) ?: "", ) - },) + }, + ) } @Deprecated("used in old swap mechanism") @@ -707,7 +710,14 @@ internal class SwapInteractorImpl @Inject constructor( } } } else { - return SwapState.SwapError(quoteDataModel.error) + val rates = getQuotes(fromToken.currency.id) + val fromTokenSwapInfo = TokenSwapInfo( + tokenAmount = amount, + amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + ?: BigDecimal.ZERO, + cryptoCurrencyStatus = fromToken, + ) + return SwapState.SwapError(fromTokenSwapInfo, quoteDataModel.error) } } @@ -787,7 +797,17 @@ internal class SwapInteractorImpl @Inject constructor( ), ) } else { - return SwapState.SwapError(it.error) + val rates = getQuotes(fromToken.currency.id) + val fromTokenSwapInfo = TokenSwapInfo( + tokenAmount = amount, + amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + ?: BigDecimal.ZERO, + cryptoCurrencyStatus = fromToken, + ) + return SwapState.SwapError( + fromTokenSwapInfo, + it.error, + ) } } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 0c98986dc1..b49765d79d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -31,7 +31,10 @@ sealed interface SwapState { val zeroAmountEquivalent: String, ) : SwapState - data class SwapError(val error: DataError) : SwapState + data class SwapError( + val fromTokenInfo: TokenSwapInfo, + val error: DataError, + ) : SwapState } sealed class PermissionDataState { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 37ef409ce5..9fc0d8a9c1 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -123,6 +123,7 @@ sealed interface SwapWarning { data class HighPriceImpact(val priceImpact: Int, val notificationConfig: NotificationConfig) : SwapWarning data class TooSmallAmountWarning(val notificationConfig: NotificationConfig) : SwapWarning data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning + data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning } enum class GenericWarningType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 7d965aa10f..4a90e77fab 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -13,6 +13,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.NetworkInfo import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation @@ -25,6 +26,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal import java.math.RoundingMode +import kotlin.math.min /** * State builder creates a specific states for SwapScreen @@ -176,6 +178,7 @@ internal class StateBuilder( balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "", isBalanceHidden = isBalanceHiddenProvider(), ), + warnings = emptyList(), fee = FeeItemState.Empty, swapButton = SwapButton(enabled = false, loading = true, onClick = {}), providerState = ProviderState.Loading(), @@ -294,6 +297,109 @@ internal class StateBuilder( ) } + fun createQuotesErrorState( + uiStateHolder: SwapStateHolder, + swapProvider: SwapProvider, + fromToken: TokenSwapInfo, + toToken: CryptoCurrencyStatus?, + dataError: DataError, + ): SwapStateHolder { + if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder + if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder + val warning = getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency) + val providerState = getProviderStateForError( + swapProvider = swapProvider, + fromToken = fromToken.cryptoCurrencyStatus.currency, + dataError = dataError, + selectionType = ProviderState.SelectionType.CLICK, + ) + val receiveCardData = toToken?.let { + SwapCardState.SwapCardData( + type = TransactionCardType.ReceiveCard(), + amountTextFieldValue = TextFieldValue( + text = "0", + ), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = toToken, + tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, + coinId = toToken.currency.network.backendId, + isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, + tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, + canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, + balance = toToken.getFormattedAmount(), + isBalanceHidden = isBalanceHiddenProvider(), + ) + } ?: SwapCardState.Empty( + type = TransactionCardType.ReceiveCard(), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + amountTextFieldValue = TextFieldValue( + text = "0", + ), + canSelectAnotherToken = true, + ) + return uiStateHolder.copy( + sendCardData = uiStateHolder.sendCardData.copy( + amountEquivalent = getFormattedFiatAmount(fromToken.amountFiat), + ), + receiveCardData = receiveCardData, + warnings = listOf(warning), + permissionState = SwapPermissionState.Empty, + fee = FeeItemState.Empty, + swapButton = SwapButton( + enabled = false, + loading = false, + onClick = actions.onSwapClick, + ), + updateInProgress = false, + providerState = providerState, + ) + } + + private fun getProviderStateForError( + swapProvider: SwapProvider, + fromToken: CryptoCurrency, + dataError: DataError, + selectionType: ProviderState.SelectionType, + ): ProviderState { + return when (dataError) { + is DataError.ExchangeTooSmallAmountError -> { + swapProvider.convertToUnavailableProviderState( + alertText = resourceReference( + R.string.express_provider_min_amount, + wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), + ), + selectionType = selectionType, + onProviderClick = actions.onProviderClick, + ) + } + else -> { + ProviderState.Empty() + } + } + } + + private fun getWarningForError(dataError: DataError, fromToken: CryptoCurrency): SwapWarning { + return when (dataError) { + is DataError.ExchangeTooSmallAmountError -> SwapWarning.TooSmallAmountWarning( + notificationConfig = NotificationConfig( + title = resourceReference( + id = R.string.warning_express_too_minimal_amount_title, + formatArgs = wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), + ), + subtitle = resourceReference(R.string.warning_express_too_minimal_amount_description), + iconResId = R.drawable.ic_alert_circle_24, + ), + ) + else -> SwapWarning.GeneralWarning( + notificationConfig = NotificationConfig( + title = resourceReference(R.string.common_error), + subtitle = resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())), + iconResId = R.drawable.ic_alert_circle_24, + ), + ) + } + } + fun createQuotesEmptyAmountState( uiStateHolder: SwapStateHolder, emptyAmountState: SwapState.EmptyAmountState, @@ -473,10 +579,9 @@ internal class StateBuilder( val fee = uiState.fee as? FeeItemState.Content ?: return uiState val fromCryptoCurrencyStatus = requireNotNull(fromToken.token) val toCryptoCurrencyStatus = requireNotNull(toToken.token) - val rate = txState.toAmount?.toBigDecimal()?.divide( - txState.fromAmount?.toBigDecimal(), + val rate = txState.toAmount?.toBigDecimal()?.calculateRate( + txState.fromAmount?.toBigDecimal() ?: BigDecimal.ZERO, toCryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, ) val fromCurrencySymbol = fromCryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toCryptoCurrencyStatus.currency.symbol @@ -510,16 +615,6 @@ internal class StateBuilder( ) } - fun mapError(uiState: SwapStateHolder, error: DataError, onClick: () -> Unit): SwapStateHolder { - return when (error) { - // todo use if needed later - // DataError.InsufficientLiquidity -> TODO() - // DataError.NoError -> TODO() - is DataError.ExchangeTooSmallAmountError -> addWarning(uiState, error.amount.toString(), true, onClick) - else -> addWarning(uiState, null, false) {} - } - } - fun addAlert(uiState: SwapStateHolder, onClick: () -> Unit): SwapStateHolder { return uiState.copy( alert = SwapWarning.GenericWarning( @@ -738,11 +833,11 @@ internal class StateBuilder( onProviderClick = onProviderSelect, selectionType = ProviderState.SelectionType.SELECT, ) - // todo handle error - is SwapState.SwapError -> provider.convertToUnavailableProviderState( - alertText = resourceReference(R.string.express_provider_min_amount, wrappedList("10")), - selectionType = ProviderState.SelectionType.NONE, - onProviderClick = onProviderSelect, + is SwapState.SwapError -> getProviderStateForError( + swapProvider = provider, + fromToken = state.fromTokenInfo.cryptoCurrencyStatus.currency, + dataError = state.error, + selectionType = ProviderState.SelectionType.SELECT, ) } } @@ -805,10 +900,9 @@ internal class StateBuilder( selectionType: ProviderState.SelectionType, onProviderClick: (String) -> Unit, ): ProviderState { - val rate = toTokenInfo.tokenAmount.value.divide( + val rate = toTokenInfo.tokenAmount.value.calculateRate( fromTokenInfo.tokenAmount.value, toTokenInfo.cryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, ) val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol @@ -834,10 +928,9 @@ internal class StateBuilder( ): ProviderState { val fromTokenInfo = state.fromTokenInfo val toTokenInfo = state.toTokenInfo - val rate = toTokenInfo.tokenAmount.value.divide( + val rate = toTokenInfo.tokenAmount.value.calculateRate( fromTokenInfo.tokenAmount.value, toTokenInfo.cryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, ) val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol @@ -898,6 +991,14 @@ internal class StateBuilder( return BigDecimalFormatter.formatFiatAmount(amount, appCurrency.code, appCurrency.symbol) } + private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { + return "${this.formatToUIRepresentation()} ${token.network.currencySymbol}" + } + + private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal { + return this.divide(to, min(decimals, MAX_DECIMALS_TO_SHOW), RoundingMode.HALF_UP) + } + private companion object { const val ADDRESS_MIN_LENGTH = 11 const val ADDRESS_FIRST_PART_LENGTH = 7 @@ -905,5 +1006,6 @@ internal class StateBuilder( private const val PRICE_IMPACT_THRESHOLD = 0.1 private const val HUNDRED_PERCENTS = 100 private const val UNKNOWN_AMOUNT_SIGN = "—" + private const val MAX_DECIMALS_TO_SHOW = 8 } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 6e89f78c3a..cde932bf0d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -43,7 +43,7 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) placeholderSearchText = stringResource(id = R.string.common_search_tokens), onSearchChange = state.onSearchEntered, onSearchDisplayClose = { state.onSearchEntered("") }, - subtitle = "", // todo add title + subtitle = stringResource(id = R.string.express_exchange_token_list_subtitle), ) }, ) @@ -55,7 +55,7 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = LazyColumn( modifier = modifier .background(color = screenBackgroundColor) - .fillMaxWidth(), + .fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { item { SpacerH8() } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index a577a98dfe..4fb07606b9 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -288,9 +288,13 @@ internal class SwapViewModel @Inject constructor( ) } is SwapState.SwapError -> { - Timber.e("SwapError when loading quotes ${state.error}") - // todo handle when change token and error - uiState = stateBuilder.mapError(uiState, state.error) { startLoadingQuotesFromLastState() } + uiState = stateBuilder.createQuotesErrorState( + uiStateHolder = uiState, + swapProvider = provider, + fromToken = state.fromTokenInfo, + toToken = dataState.toCryptoCurrency, + dataError = state.error, + ) } } } From 1466ad2e6e2ee21df29581f432e53a7d5c75c812 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 29 Nov 2023 19:19:49 +0200 Subject: [PATCH 084/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 49 ++++++++++++++++--- .../swap/domain/models/ui/SwapState.kt | 1 + 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 8669d31ece..31615ff878 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,6 +1,8 @@ package com.tangem.feature.swap.domain import arrow.core.getOrElse +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase @@ -372,7 +374,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend = currencyToSend, currencyToGet = currencyToGet, amount = amount, - fee = fee, + txFee = fee, swapProvider = swapProvider, userWalletId = requireNotNull(getSelectedWallet()).walletId, ) @@ -474,7 +476,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, amount: SwapAmount, - fee: TxFee, + txFee: TxFee, swapProvider: SwapProvider, userWalletId: UserWalletId, ): TxState { @@ -492,7 +494,7 @@ internal class SwapInteractorImpl @Inject constructor( val txData = walletManagersFacade.createTransaction( amount = amount.value.convertToAmount(currencyToSend.currency), - fee = Fee.Common(fee.feeValue.convertToAmount(currencyToSend.currency)), + fee = getFeeForTransaction(txFee), memo = null, destination = (exchangeData.dataModel?.transaction as ExpressTransactionModel.CEX).txTo, userWalletId = userWalletId, @@ -535,6 +537,27 @@ internal class SwapInteractorImpl @Inject constructor( ) } + private fun getFeeForTransaction(fee: TxFee): Fee { + val feeAmountValue = fee.feeValue + val feeAmount = Amount( + value = fee.feeValue, + currencySymbol = fee.cryptoSymbol, + decimals = fee.decimals, + type = AmountType.Coin + ) + + return if (fee.gasLimit != 0) { + Fee.Ethereum( + amount = feeAmount, + gasLimit = fee.gasLimit.toBigInteger(), + gasPrice = (feeAmountValue / fee.gasLimit.toBigDecimal()).toBigInteger() + ) + } else { + Fee.Common(feeAmount) + } + + } + @Deprecated("used in old swap mechanism") override fun getTokenBalance(networkId: String, token: CryptoCurrency): SwapAmount { return cache.getBalanceForToken( @@ -993,13 +1016,14 @@ internal class SwapInteractorImpl @Inject constructor( val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" } val priorityFiatFee = requireNotNull(feesFiat.getOrNull(1)) { "feesFiat item 1 couldn't be null" } val networkCurrency = userWalletManager.getNetworkCurrency(networkId) + val decimals = transactionManager.getNativeTokenDecimals(networkId) val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = normalFeeValue, - decimals = transactionManager.getNativeTokenDecimals(networkId), + decimals = decimals, ) val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = priorityFeeValue, - decimals = transactionManager.getNativeTokenDecimals(networkId), + decimals = decimals, ) return TxFeeState.MultipleFeeState( normalFee = TxFee( @@ -1007,6 +1031,7 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = normalFeeGas, feeFiatFormatted = normalFiatFee, feeCryptoFormatted = normalCryptoFee, + decimals = decimals, cryptoSymbol = networkCurrency, feeType = FeeType.NORMAL, ), @@ -1015,6 +1040,7 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = priorityFeeGas, feeFiatFormatted = priorityFiatFee, feeCryptoFormatted = priorityCryptoFee, + decimals = decimals, cryptoSymbol = networkCurrency, feeType = FeeType.PRIORITY, ), @@ -1027,9 +1053,10 @@ internal class SwapInteractorImpl @Inject constructor( val networkCurrency = userWalletManager.getNetworkCurrency(networkId) val feesFiat = getFormattedFiatFees(networkId, normalFeeValue) val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" } + val decimals = transactionManager.getNativeTokenDecimals(networkId) val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = normalFeeValue, - decimals = transactionManager.getNativeTokenDecimals(networkId), + decimals = decimals, ) return TxFeeState.SingleFeeState( fee = TxFee( @@ -1037,6 +1064,7 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = normalFeeGas, feeFiatFormatted = normalFiatFee, feeCryptoFormatted = normalCryptoFee, + decimals = decimals, cryptoSymbol = networkCurrency, feeType = FeeType.NORMAL, ), @@ -1045,19 +1073,21 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun TransactionFee.toTxFeeState(networkId: String): TxFeeState { val networkCurrency = userWalletManager.getNetworkCurrency(networkId) + val decimals = transactionManager.getNativeTokenDecimals(networkId) return when (this) { is TransactionFee.Choosable -> { val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO val feePriority = this.priority.amount.value ?: BigDecimal.ZERO val normalFiatValue = getFormattedFiatFees(networkId, feeNormal)[0] val priorityFiatValue = getFormattedFiatFees(networkId, feePriority)[0] + val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = feeNormal, - decimals = transactionManager.getNativeTokenDecimals(networkId), + decimals = decimals ) val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = feePriority, - decimals = transactionManager.getNativeTokenDecimals(networkId), + decimals = decimals, ) TxFeeState.MultipleFeeState( normalFee = TxFee( @@ -1065,6 +1095,7 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = this.normal.getGasLimit(), feeFiatFormatted = normalFiatValue, feeCryptoFormatted = normalCryptoFee, + decimals = decimals, cryptoSymbol = networkCurrency, feeType = FeeType.NORMAL, ), @@ -1073,6 +1104,7 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = this.priority.getGasLimit(), feeFiatFormatted = priorityFiatValue, feeCryptoFormatted = priorityCryptoFee, + decimals = decimals, cryptoSymbol = networkCurrency, feeType = FeeType.PRIORITY, ), @@ -1091,6 +1123,7 @@ internal class SwapInteractorImpl @Inject constructor( gasLimit = this.normal.getGasLimit(), feeFiatFormatted = normalFiatValue, feeCryptoFormatted = normalCryptoFee, + decimals = decimals, cryptoSymbol = networkCurrency, feeType = FeeType.NORMAL, ), diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index b49765d79d..93a5795340 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -90,6 +90,7 @@ data class TxFee( val gasLimit: Int, val feeFiatFormatted: String, val feeCryptoFormatted: String, + val decimals: Int, val cryptoSymbol: String, val feeType: FeeType, ) From 45193e4878a9d28321604f492c67f628414cd57b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 29 Nov 2023 20:20:08 +0300 Subject: [PATCH 085/139] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings-blockchain.xml | 1 - core/res/src/main/res/values-fr/strings-blockchain.xml | 1 - core/res/src/main/res/values-it/strings-blockchain.xml | 1 - core/res/src/main/res/values-zh-rTW/strings-blockchain.xml | 1 - 4 files changed, 4 deletions(-) diff --git a/core/res/src/main/res/values-de/strings-blockchain.xml b/core/res/src/main/res/values-de/strings-blockchain.xml index b6c425deeb..d438a29cd8 100644 --- a/core/res/src/main/res/values-de/strings-blockchain.xml +++ b/core/res/src/main/res/values-de/strings-blockchain.xml @@ -1,7 +1,6 @@ Erhalt der Gebühr fehlgeschlagen - Laden Sie %1$s+ %2$s auf um ein Konto zu erstellen Minimaler Betrag ist %s Restbestand zu klein Falsche Gebühr diff --git a/core/res/src/main/res/values-fr/strings-blockchain.xml b/core/res/src/main/res/values-fr/strings-blockchain.xml index 42c3c19b03..0f4d6dccec 100644 --- a/core/res/src/main/res/values-fr/strings-blockchain.xml +++ b/core/res/src/main/res/values-fr/strings-blockchain.xml @@ -1,7 +1,6 @@ Échec de réception des commissions - Pour créer un compte, téléchargez %1$s+ %2$s Le montant minimal est de %s Le reste est trop petit Commission non valide diff --git a/core/res/src/main/res/values-it/strings-blockchain.xml b/core/res/src/main/res/values-it/strings-blockchain.xml index 2d50632130..338c03f0da 100644 --- a/core/res/src/main/res/values-it/strings-blockchain.xml +++ b/core/res/src/main/res/values-it/strings-blockchain.xml @@ -1,7 +1,6 @@ Impossibile ottenere la commissione - Scarica %1$s+ %2$s per creare un account L\'importo minimo è di %s L\'importo residuo è molto basso Commissione non valida diff --git a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml index 5aa30cef8f..607268ec3a 100644 --- a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml +++ b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml @@ -4,7 +4,6 @@ 遺留資產 獲取費用失敗 由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。 - 加載 %1$s+ %2$s 以創建帳戶 目標帳戶未激活。發送 %s 或更多以激活帳戶 最小數量是 %s 更動太小 From ed5d5b1b33b59f5a7282047c8aa271d2b3b10f4c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 10:36:39 +0400 Subject: [PATCH 086/139] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 11 ++- .../chooseaddress/ChooseAddressBottomSheet.kt | 3 +- .../tokenreceive/AddressMappers.kt | 43 ++++++++++ .../bottomsheets/tokenreceive/AddressModel.kt | 5 +- .../tokenreceive/TokenReceiveBottomSheet.kt | 12 +-- .../TokenReceiveBottomSheetConfig.kt | 3 +- .../core/ui/extensions/BlockchainIcons.kt | 1 - .../data/tokens/utils/NetworkStatusFactory.kt | 24 +++++- .../domain/common/extensions/Blockchain.kt | 5 +- .../walletmanager/WalletManagersFacade.kt | 6 ++ .../domain/walletmanager/model/Address.kt | 11 +++ .../model/UpdateWalletManagerResult.kt | 8 +- .../utils/UpdateWalletManagerResultFactory.kt | 30 ++++--- .../tokens/model/CryptoCurrencyStatus.kt | 8 +- .../domain/tokens/model/NetworkAddress.kt | 30 ++++--- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 4 +- .../tangem/domain/tokens/mock/MockNetworks.kt | 16 +++- .../domain/tokens/mock/MockTokensStates.kt | 17 +++- .../state/factory/TokenDetailsStateFactory.kt | 27 ++---- .../viewmodels/TokenDetailsViewModel.kt | 46 +++++----- .../wallet/viewmodels/WalletViewModel.kt | 84 ++++++++----------- .../WalletCurrencyActionsClickIntents.kt | 58 ++++++------- gradle/dependencies.toml | 4 +- 23 files changed, 269 insertions(+), 187 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressMappers.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/Address.kt diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index c3092a3714..33a23f9750 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -14,6 +14,7 @@ import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.features.send.api.navigation.SendRouter import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -123,7 +124,10 @@ class TradeCryptoMiddleware { } private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) { - val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return + val networkAddress = action.cryptoCurrencyStatus.value.networkAddress + ?.defaultAddress + ?.let(NetworkAddress.Address::value) + ?: return val status = action.cryptoCurrencyStatus val currency = status.currency @@ -206,7 +210,10 @@ class TradeCryptoMiddleware { } private fun proceedNewSellAction(action: TradeCryptoAction.New.Sell) { - val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return + val networkAddress = action.cryptoCurrencyStatus.value.networkAddress + ?.defaultAddress + ?.let(NetworkAddress.Address::value) + ?: return val currency = action.cryptoCurrencyStatus.currency store.state.globalState.exchangeManager.getUrl( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt index 4bec68b142..098950828e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.SimpleSettingsRow import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @Composable @@ -24,7 +25,7 @@ private fun ChooseAddressBottomSheetContent(content: ChooseAddressBottomSheetCon ) { content.addressModels.forEach { addressModel -> SimpleSettingsRow( - title = addressModel.type.name, + title = addressModel.displayName.resolveReference(), icon = R.drawable.ic_arrow_top_right_24, onItemsClick = { content.onClick(addressModel) }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressMappers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressMappers.kt new file mode 100644 index 0000000000..dfd23f44c2 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressMappers.kt @@ -0,0 +1,43 @@ +package com.tangem.core.ui.components.bottomsheets.tokenreceive + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress + +private const val DSC_ADDRESS_NAME = "DSC" +private const val DEL_ADDRESS_NAME = "Main" + +private const val LEGACY_ADDRESS_NAME = "Legacy" +private const val DEFAULT_ADDRESS_NAME = "Default" + +fun Set.mapToAddressModels(currency: CryptoCurrency): List = this + .sortedBy { it.type.ordinal } + .map { address -> + AddressModel( + displayName = currency.network.getAddressDisplayName(address.type), + value = address.value, + type = when (address.type) { + NetworkAddress.Address.Type.Primary -> AddressModel.Type.Default + NetworkAddress.Address.Type.Secondary -> AddressModel.Type.Legacy + }, + ) + } + +private fun Network.getAddressDisplayName(addressType: NetworkAddress.Address.Type): TextReference { + return when (id.value) { + "decimal", "decimal/test" -> { + when (addressType) { + NetworkAddress.Address.Type.Primary -> stringReference(value = DEL_ADDRESS_NAME) + NetworkAddress.Address.Type.Secondary -> stringReference(value = DSC_ADDRESS_NAME) + } + } + else -> { + when (addressType) { + NetworkAddress.Address.Type.Primary -> stringReference(value = DEFAULT_ADDRESS_NAME) + NetworkAddress.Address.Type.Secondary -> stringReference(value = LEGACY_ADDRESS_NAME) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt index 0314b21846..8571fdfa35 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt @@ -1,8 +1,11 @@ package com.tangem.core.ui.components.bottomsheets.tokenreceive +import com.tangem.core.ui.extensions.TextReference + data class AddressModel( + val displayName: TextReference, val value: String, - val type: Type = Type.Default, + val type: Type, ) { enum class Type { Legacy, Default diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt index 704418b5ec..1057a43847 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.* +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -26,6 +26,7 @@ import com.tangem.core.ui.components.SecondaryButtonIconStart import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.rememberQrPainters +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme @@ -181,13 +182,6 @@ private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String return if (content.addresses.size < 2) { content.name } else { - "${ - stringResource( - id = when (content.addresses[index].type) { - AddressModel.Type.Default -> R.string.address_type_default - AddressModel.Type.Legacy -> R.string.address_type_legacy - }, - ) - } ${content.name}" + "${content.addresses[index].displayName.resolveReference()} ${content.name}" } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt index c2531a839d..a0a649302c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt @@ -1,12 +1,13 @@ package com.tangem.core.ui.components.bottomsheets.tokenreceive import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList class TokenReceiveBottomSheetConfig( val name: String, val symbol: String, val network: String, - val addresses: List, + val addresses: ImmutableList, val onCopyClick: () -> Unit, val onShareClick: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index e418da8ea3..95a8e5e94e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -28,7 +28,6 @@ fun getActiveIconRes(blockchainId: String): Int { "GNO" -> R.drawable.img_gnosis_22 "ETH-Pow", "ETH-Pow/test" -> R.drawable.img_eth_pow_22 "ETH-Fair" -> R.drawable.img_eth_fair_22 - "NEAR", "NEAR/test" -> R.drawable.img_near_22 "Polkadot", "Polkadot/test" -> R.drawable.img_polkadot_22 "Kusama" -> R.drawable.img_kusama_22 "OPTIMISM", "OPTIMISM/test" -> R.drawable.img_optimism_22 diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index 47979ec3a4..dbbf3fb10a 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils import com.tangem.domain.tokens.model.* import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.walletmanager.model.Address import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult @@ -20,12 +21,12 @@ internal class NetworkStatusFactory { is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable is UpdateWalletManagerResult.NoAccount -> NetworkStatus.NoAccount( - address = getNetworkAddress(result.defaultAddress, result.addresses), + address = getNetworkAddress(result.selectedAddress, result.addresses), amountToCreateAccount = result.amountToCreateAccount, errorMessage = result.errorMessage, ) is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified( - address = getNetworkAddress(result.defaultAddress, result.addresses), + address = getNetworkAddress(result.selectedAddress, result.addresses), amounts = formatAmounts(result.currenciesAmounts, currencies), pendingTransactions = formatTransactions( transactions = result.currentTransactions, @@ -92,11 +93,26 @@ internal class NetworkStatusFactory { return transactions.mapTo(hashSetOf()) { it.txHistoryItem } } - private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set): NetworkAddress { + private fun getNetworkAddress(selectedAddress: String, availableAddresses: Set
): NetworkAddress { + val defaultAddress = availableAddresses + .firstOrNull { it.value == selectedAddress } + ?.let(::mapToDomainAddress) + + requireNotNull(defaultAddress) { "Selected address must not be null" } + return if (availableAddresses.size != 1) { - NetworkAddress.Selectable(defaultAddress, availableAddresses) + NetworkAddress.Selectable(defaultAddress, availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress)) } else { NetworkAddress.Single(defaultAddress) } } + + private fun mapToDomainAddress(address: Address): NetworkAddress.Address { + val type = when (address.type) { + Address.Type.Primary -> NetworkAddress.Address.Type.Primary + Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary + } + + return NetworkAddress.Address(address.value, type) + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 2e72d3e67f..e4fb4f23a1 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -195,8 +195,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Chia, Blockchain.ChiaTestnet -> "chia" Blockchain.Near -> "near" Blockchain.NearTestnet -> "near/test" - Blockchain.Decimal -> "decimal" - Blockchain.DecimalTestnet -> "decimal/test" + Blockchain.Decimal, Blockchain.DecimalTestnet -> "decimal" Blockchain.Unknown -> "unknown" } } @@ -224,6 +223,4 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Ducatus, - Blockchain.Decimal, - Blockchain.DecimalTestnet, ) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index f2b89b51a6..dba5d089eb 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -116,6 +116,7 @@ interface WalletManagersFacade { * @param userWalletId selected wallet id * @param network network of currency */ + @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
/** Returns list of all addresses for all currencies in selected wallet @@ -123,6 +124,7 @@ interface WalletManagersFacade { * @param userWalletId selected wallet id * @param network required to create wallet manager */ + @Deprecated("Use NetworkAddress from CryptoCurrencyStatus") suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set
/** @@ -156,6 +158,7 @@ interface WalletManagersFacade { * @param userWalletId selected wallet id * @param network network of currency */ + @Deprecated("Will be removed in future") suspend fun getFee( amount: Amount, destination: String, @@ -171,6 +174,7 @@ interface WalletManagersFacade { * @param userWalletId selected wallet id * @param network network of currency */ + @Deprecated("Will be removed in future") suspend fun validateTransaction( amount: Amount, fee: Amount?, @@ -189,6 +193,7 @@ interface WalletManagersFacade { * @param network network of currency */ @Suppress("LongParameterList") + @Deprecated("Will be removed in future") suspend fun createTransaction( amount: Amount, fee: Fee, @@ -206,6 +211,7 @@ interface WalletManagersFacade { * @param userWalletId selected wallet id * @param network network of currency */ + @Deprecated("Will be removed in future") suspend fun sendTransaction( txData: TransactionData, signer: CommonSigner, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/Address.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/Address.kt new file mode 100644 index 0000000000..c495bf7c9a --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/Address.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.walletmanager.model + +data class Address( + val value: String, + val type: Type, +) { + + enum class Type { + Primary, Secondary, + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt index ae14d752e1..dd1891105e 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt @@ -9,15 +9,15 @@ sealed class UpdateWalletManagerResult { object Unreachable : UpdateWalletManagerResult() data class Verified( - val defaultAddress: String, - val addresses: Set, + val selectedAddress: String, + val addresses: Set
, val currenciesAmounts: Set, val currentTransactions: Set, ) : UpdateWalletManagerResult() data class NoAccount( - val defaultAddress: String, - val addresses: Set, + val selectedAddress: String, + val addresses: Set
, val amountToCreateAccount: BigDecimal, val errorMessage: String, ) : UpdateWalletManagerResult() diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt index 1e9c032f6c..4a67053404 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -1,15 +1,17 @@ package com.tangem.domain.walletmanager.utils import com.tangem.blockchain.common.* -import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.common.address.AddressType import com.tangem.domain.common.extensions.amountToCreateAccount import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.walletmanager.model.Address import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import timber.log.Timber import java.math.BigDecimal import java.util.concurrent.TimeUnit +import com.tangem.blockchain.common.address.Address as SdkAddress internal class UpdateWalletManagerResultFactory { @@ -18,7 +20,7 @@ internal class UpdateWalletManagerResultFactory { val addresses = getAvailableAddresses(wallet.addresses) return UpdateWalletManagerResult.Verified( - defaultAddress = wallet.address, + selectedAddress = wallet.address, addresses = addresses, currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()), currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()), @@ -30,7 +32,7 @@ internal class UpdateWalletManagerResultFactory { val addresses = getAvailableAddresses(wallet.addresses) return UpdateWalletManagerResult.Verified( - defaultAddress = wallet.address, + selectedAddress = wallet.address, addresses = addresses, currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens), currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()), @@ -48,7 +50,7 @@ internal class UpdateWalletManagerResultFactory { UpdateWalletManagerResult.Unreachable } else { UpdateWalletManagerResult.NoAccount( - defaultAddress = wallet.address, + selectedAddress = wallet.address, addresses = getAvailableAddresses(wallet.addresses), amountToCreateAccount = amountToCreateAccount, errorMessage = customMessage, @@ -70,7 +72,7 @@ internal class UpdateWalletManagerResultFactory { } private fun getCurrentTransactions( - walletAddresses: Set, + walletAddresses: Set
, recentTransactions: Set, ): Set { val unconfirmedTransactions = recentTransactions.filter { @@ -95,7 +97,7 @@ internal class UpdateWalletManagerResultFactory { } private fun createCurrencyTransaction( - walletAddresses: Set, + walletAddresses: Set
, data: TransactionData, ): CryptoCurrencyTransaction? { return when (val type = data.amount.type) { @@ -115,11 +117,11 @@ internal class UpdateWalletManagerResultFactory { } } - private fun createTxHistoryItem(walletAddresses: Set, data: TransactionData): TxHistoryItem? { + private fun createTxHistoryItem(walletAddresses: Set
, data: TransactionData): TxHistoryItem? { val hash = data.hash ?: return null val millis = data.date?.timeInMillis ?: return null val amount = getTransactionAmountValue(data.amount) ?: return null - val isOutgoing = data.sourceAddress in walletAddresses + val isOutgoing = data.sourceAddress in walletAddresses.map { it.value } return TxHistoryItem( txHash = hash, @@ -141,8 +143,16 @@ internal class UpdateWalletManagerResultFactory { ) } - private fun getAvailableAddresses(addresses: Set
): Set { - return addresses.mapTo(hashSetOf()) { it.value } + private fun getAvailableAddresses(addresses: Set): Set
{ + return addresses.mapTo(hashSetOf()) { sdkAddress -> + Address( + value = sdkAddress.value, + type = when (sdkAddress.type) { + AddressType.Default -> Address.Type.Primary + AddressType.Legacy -> Address.Type.Secondary + }, + ) + } } private fun getCurrencyAmountValue(amount: Amount): BigDecimal? { diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 5ba1af68eb..23700e3d7b 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -78,7 +78,7 @@ data class CryptoCurrencyStatus( override val fiatAmount: BigDecimal?, override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, - override val networkAddress: NetworkAddress?, + override val networkAddress: NetworkAddress, ) : Status(isError = false) { override val amount: BigDecimal = BigDecimal.ZERO @@ -102,7 +102,7 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, - override val networkAddress: NetworkAddress?, + override val networkAddress: NetworkAddress, ) : Status(isError = false) /** @@ -123,7 +123,7 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, - override val networkAddress: NetworkAddress?, + override val networkAddress: NetworkAddress, ) : Status(isError = false) /** @@ -138,6 +138,6 @@ data class CryptoCurrencyStatus( override val amount: BigDecimal, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, - override val networkAddress: NetworkAddress?, + override val networkAddress: NetworkAddress, ) : Status(isError = false) } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt index 542d48485e..70d50581d4 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkAddress.kt @@ -6,18 +6,19 @@ package com.tangem.domain.tokens.model sealed class NetworkAddress { /** The default or currently selected network address. */ - abstract val defaultAddress: String + abstract val defaultAddress: Address + + /** The set of available network addresses to choose from. */ + abstract val availableAddresses: Set
/** * Represents a single static network address. * * @property defaultAddress The static network address. */ - data class Single(override val defaultAddress: String) : NetworkAddress() { + data class Single(override val defaultAddress: Address) : NetworkAddress() { - init { - checkDefaultAddress() - } + override val availableAddresses: Set
= setOf(defaultAddress) } /** @@ -27,17 +28,26 @@ sealed class NetworkAddress { * @property availableAddresses The set of available network addresses to choose from. */ data class Selectable( - override val defaultAddress: String, - val availableAddresses: Set, + override val defaultAddress: Address, + override val availableAddresses: Set
, ) : NetworkAddress() { init { - checkDefaultAddress() require(availableAddresses.isNotEmpty()) { "Available network addresses must not be empty" } } } - protected fun checkDefaultAddress() { - require(defaultAddress.isNotBlank()) { "Selected network address must not be blank" } + data class Address( + val value: String, + val type: Type, + ) { + + enum class Type { + Primary, Secondary, + } + + init { + require(value.isNotBlank()) { "Address value must not be blank" } + } } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 6cdc76877c..b5c027ac68 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -118,7 +118,9 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { amount = BigDecimal.TEN, hasCurrentNetworkTransactions = false, pendingTransactions = emptySet(), - networkAddress = NetworkAddress.Single(defaultAddress = "mock"), + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 979479e010..369bb8b5b3 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -51,7 +51,9 @@ internal object MockNetworks { network = network3, value = NetworkStatus.NoAccount( amountToCreateAccount = amountToCreateAccount, - address = NetworkAddress.Single(defaultAddress = "mock"), + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), errorMessage = "", ), ) @@ -67,7 +69,9 @@ internal object MockNetworks { MockTokens.token3.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN), ), pendingTransactions = emptyMap(), - address = NetworkAddress.Single(defaultAddress = "mock"), + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) @@ -80,7 +84,9 @@ internal object MockNetworks { MockTokens.token6.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN), ), pendingTransactions = emptyMap(), - address = NetworkAddress.Single(defaultAddress = "mock"), + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) @@ -94,7 +100,9 @@ internal object MockNetworks { MockTokens.token10.id to CryptoCurrencyAmountStatus.Loaded(BigDecimal.TEN), ), pendingTransactions = emptyMap(), - address = NetworkAddress.Single(defaultAddress = "mock"), + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 38338ba598..bb9071f999 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -3,6 +3,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.NetworkStatus import java.math.BigDecimal @@ -64,7 +65,9 @@ internal object MockTokensStates { priceChange = MockQuotes.quote7.priceChange, fiatRate = MockQuotes.quote7.fiatRate, amountToCreateAccount = MockNetworks.amountToCreateAccount, - networkAddress = null, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) @@ -75,7 +78,9 @@ internal object MockTokensStates { priceChange = MockQuotes.quote8.priceChange, fiatRate = MockQuotes.quote8.fiatRate, amountToCreateAccount = MockNetworks.amountToCreateAccount, - networkAddress = null, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) @@ -86,7 +91,9 @@ internal object MockTokensStates { priceChange = MockQuotes.quote9.priceChange, fiatRate = MockQuotes.quote9.fiatRate, amountToCreateAccount = MockNetworks.amountToCreateAccount, - networkAddress = null, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) @@ -97,7 +104,9 @@ internal object MockTokensStates { priceChange = MockQuotes.quote10.priceChange, fiatRate = MockQuotes.quote10.fiatRate, amountToCreateAccount = MockNetworks.amountToCreateAccount, - networkAddress = null, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), + ), ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 1494ed003f..997a6e985a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -2,12 +2,11 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import androidx.paging.PagingData import arrow.core.Either -import com.tangem.blockchain.common.address.Address import com.tangem.common.Provider import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent @@ -17,6 +16,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryItem @@ -174,7 +174,7 @@ internal class TokenDetailsStateFactory( fun getStateWithReceiveBottomSheet( currency: CryptoCurrency, - addresses: List
, + networkAddress: NetworkAddress, sendCopyAnalyticsEvent: () -> Unit, sendShareAnalyticsEvent: () -> Unit, ): TokenDetailsState { @@ -186,12 +186,7 @@ internal class TokenDetailsStateFactory( name = currency.name, symbol = currency.symbol, network = currency.network.name, - addresses = addresses.map { - AddressModel( - value = it.value, - type = AddressModel.Type.valueOf(it.type.name), - ) - }, + addresses = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(), onCopyClick = sendCopyAnalyticsEvent, onShareClick = sendShareAnalyticsEvent, ), @@ -199,20 +194,16 @@ internal class TokenDetailsStateFactory( ) } - fun getStateWithChooseAddressBottomSheet(addresses: List
): TokenDetailsState { + fun getStateWithChooseAddressBottomSheet( + currency: CryptoCurrency, + networkAddress: NetworkAddress, + ): TokenDetailsState { return currentStateProvider().copy( bottomSheetConfig = TangemBottomSheetConfig( isShow = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = ChooseAddressBottomSheetConfig( - addressModels = addresses - .map { address -> - AddressModel( - value = address.value, - type = AddressModel.Type.valueOf(address.type.name), - ) - } - .toImmutableList(), + addressModels = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(), onClick = clickIntents::onAddressTypeSelected, ), ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index b1496b2e0c..7be83bff9e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -23,12 +23,12 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -67,7 +67,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, - private val walletManagersFacade: WalletManagersFacade, private val isDemoCardUseCase: IsDemoCardUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, @@ -303,19 +302,15 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onReceiveClick() { - analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol)) + val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return viewModelScope.launch(dispatchers.io) { - val addresses = walletManagersFacade.getAddress( - userWalletId = userWalletId, - network = cryptoCurrency.network, - ) - - analyticsEventsHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened) + analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol)) + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened) uiState = stateFactory.getStateWithReceiveBottomSheet( currency = cryptoCurrency, - addresses = addresses, + networkAddress = networkAddress, sendCopyAnalyticsEvent = { analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) }, @@ -378,22 +373,23 @@ internal class TokenDetailsViewModel @Inject constructor( } private fun openExplorer() { - viewModelScope.launch(dispatchers.io) { - val addresses = walletManagersFacade.getAddress( - userWalletId = userWalletId, - network = cryptoCurrency.network, - ) + val currencyStatus = cryptoCurrencyStatus ?: return - if (addresses.size == 1) { - router.openUrl( - url = getExploreUrlUseCase( - userWalletId = userWalletId, - currency = cryptoCurrency, - addressType = AddressType.Default, - ), - ) - } else { - uiState = stateFactory.getStateWithChooseAddressBottomSheet(addresses = addresses) + viewModelScope.launch(dispatchers.io) { + when (val addresses = currencyStatus.value.networkAddress) { + is NetworkAddress.Selectable -> { + uiState = stateFactory.getStateWithChooseAddressBottomSheet(cryptoCurrency, addresses) + } + is NetworkAddress.Single -> { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + addressType = AddressType.Default, + ), + ) + } + null -> Unit } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 1cb74caecf..6534df2c8c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -19,6 +19,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList @@ -42,10 +43,7 @@ import com.tangem.domain.settings.* import com.tangem.domain.tokens.* import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkGroup -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase @@ -689,20 +687,11 @@ internal class WalletViewModel @Inject constructor( } override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - val state = uiState as? WalletState.ContentState ?: return - analyticsEventsHandler.send( event = TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrencyStatus.currency.symbol), ) viewModelScope.launch(dispatchers.io) { - val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) - - val addresses = walletManagersFacade.getAddress( - userWalletId = userWallet.walletId, - network = cryptoCurrencyStatus.currency.network, - ) - analyticsEventsHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened) val currency = cryptoCurrencyStatus.currency @@ -711,12 +700,11 @@ internal class WalletViewModel @Inject constructor( name = currency.name, symbol = currency.symbol, network = currency.network.name, - addresses = addresses.map { - AddressModel( - value = it.value, - type = AddressModel.Type.valueOf(it.type.name), - ) - }, + addresses = cryptoCurrencyStatus.value.networkAddress + ?.availableAddresses + ?.mapToAddressModels(currency) + .orEmpty() + .toImmutableList(), onCopyClick = { analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) }, @@ -821,41 +809,39 @@ internal class WalletViewModel @Inject constructor( private fun openExplorer() { val state = uiState as? WalletState.ContentState ?: return - val currency = singleWalletCryptoCurrencyStatus?.currency ?: return + val currencyStatus = singleWalletCryptoCurrencyStatus ?: return + val currency = currencyStatus.currency viewModelScope.launch(dispatchers.main) { val userWalletId = getWallet(state.walletsListConfig.selectedWalletIndex).walletId - val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) - - if (addresses.size == 1) { - router.openUrl( - url = getExploreUrlUseCase( - userWalletId = userWalletId, - currency = currency, - addressType = AddressType.Default, - ), - ) - } else { - uiState = stateFactory.getStateWithOpenWalletBottomSheet( - ChooseAddressBottomSheetConfig( - addressModels = addresses - .map { address -> - AddressModel( - value = address.value, - type = AddressModel.Type.valueOf(address.type.name), + when (val addresses = currencyStatus.value.networkAddress) { + is NetworkAddress.Selectable -> { + uiState = stateFactory.getStateWithOpenWalletBottomSheet( + ChooseAddressBottomSheetConfig( + addressModels = addresses.availableAddresses + .mapToAddressModels(currency) + .toImmutableList(), + onClick = { + onAddressTypeSelected( + userWalletId = userWalletId, + currency = currency, + addressModel = it, ) - } - .toImmutableList(), - onClick = { - onAddressTypeSelected( - userWalletId = userWalletId, - currency = currency, - addressModel = it, - ) - }, - ), - ) + }, + ), + ) + } + is NetworkAddress.Single -> { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = currency, + addressType = AddressType.Default, + ), + ) + } + null -> Unit } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 9690fa7684..789557c99e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -1,12 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents -import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -20,6 +20,7 @@ import com.tangem.domain.tokens.RemoveCurrencyUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.walletconnect.WalletConnectActions @@ -141,15 +142,15 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) viewModelScope.launch(dispatchers.main) { - val currency = cryptoCurrencyStatus.currency - val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) - analyticsEventHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened) stateHolder.update( OpenBottomSheetTransformer( userWalletId = userWalletId, - content = createReceiveBottomSheetContent(currency, addresses), + content = createReceiveBottomSheetContent( + currency = cryptoCurrencyStatus.currency, + addresses = cryptoCurrencyStatus.value.networkAddress?.availableAddresses ?: return@launch, + ), onDismissBottomSheet = { stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) }, @@ -160,18 +161,13 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun createReceiveBottomSheetContent( currency: CryptoCurrency, - addresses: List
, + addresses: Set, ): TangemBottomSheetConfigContent { return TokenReceiveBottomSheetConfig( name = currency.name, symbol = currency.symbol, network = currency.network.name, - addresses = addresses.map { address -> - AddressModel( - value = address.value, - type = AddressModel.Type.valueOf(address.type.name), - ) - }, + addresses = addresses.mapToAddressModels(currency).toImmutableList(), onCopyClick = { analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) }, @@ -329,40 +325,36 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( val userWalletId = stateHolder.getSelectedWalletId() viewModelScope.launch(dispatchers.main) { - val currency = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId)?.currency ?: return@launch - val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network) + val currencyStatus = getPrimaryCurrencyStatusUpdatesUseCase.unwrap(userWalletId) ?: return@launch - if (addresses.size == 1) { - router.openUrl( - url = getExploreUrlUseCase( - userWalletId = userWalletId, - currency = currency, - addressType = AddressType.Default, - ), - ) - } else { - showChooseAddressBottomSheet(userWalletId, addresses, currency) + when (val addresses = currencyStatus.value.networkAddress) { + is NetworkAddress.Selectable -> { + showChooseAddressBottomSheet(userWalletId, addresses.availableAddresses, currencyStatus.currency) + } + is NetworkAddress.Single -> { + router.openUrl( + url = getExploreUrlUseCase( + userWalletId = userWalletId, + currency = currencyStatus.currency, + addressType = AddressType.Default, + ), + ) + } + null -> Unit } } } private fun showChooseAddressBottomSheet( userWalletId: UserWalletId, - addresses: List
, + addresses: Set, currency: CryptoCurrency, ) { stateHolder.update( OpenBottomSheetTransformer( userWalletId = userWalletId, content = ChooseAddressBottomSheetConfig( - addressModels = addresses - .map { address -> - AddressModel( - value = address.value, - type = AddressModel.Type.valueOf(address.type.name), - ) - } - .toImmutableList(), + addressModels = addresses.mapToAddressModels(currency).toImmutableList(), onClick = { onAddressTypeSelected( userWalletId = userWalletId, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 95e9a29cf5..fb9f8dad5f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,10 +88,10 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-396" +tangemBlockchainSdk = "develop-401" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-312" -#tangemCardSdk = "0.0.1" # Keep it! - used for local builds +#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem # region Tools From e7cd5f4c2efbe4295a7b5ec9874d39a059089864 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 11:42:17 +0300 Subject: [PATCH 087/139] Updated on 2026-08-14 --- .../feature/swap/ui/SwapSelectTokenScreen.kt | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index cde932bf0d..9d90e20505 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -11,7 +12,10 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.Strings import com.tangem.core.ui.components.* @@ -34,7 +38,12 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) .systemBarsPadding() .background(color = TangemTheme.colors.background.secondary), content = { padding -> - ListOfTokens(state = state, Modifier.padding(padding)) + val modifier = Modifier.padding(padding) + if (state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty()) { + EmptyTokensList(modifier) + } else { + ListOfTokens(state = state, modifier = modifier) + } }, topBar = { ExpandableSearchView( @@ -49,6 +58,36 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) ) } +@Composable +private fun EmptyTokensList(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background(TangemTheme.colors.background.secondary) + .fillMaxSize() + ) { + Column(modifier = Modifier.align(Alignment.Center)) { + Image( + modifier = Modifier + .size(TangemTheme.dimens.size64) + .align(Alignment.CenterHorizontally), + painter = painterResource(id = R.drawable.ic_no_token_44), + colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive), + contentDescription = null, + ) + Text( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16) + .padding(horizontal = TangemTheme.dimens.spacing30) + .align(Alignment.CenterHorizontally), + text = stringResource(id = R.string.exchange_tokens_empty_tokens), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center + ) + } + } +} + @Composable private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) { val screenBackgroundColor = TangemTheme.colors.background.secondary @@ -246,4 +285,12 @@ private fun TokenScreenPreview() { onBack = {}, ) } +} + +@Preview +@Composable +private fun EmptyTokensListPreview() { + TangemTheme(isDark = false) { + EmptyTokensList() + } } \ No newline at end of file From 97f9283cea52862be282044136c9040c7ab82716 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 12:17:43 +0300 Subject: [PATCH 088/139] Updated on 2026-08-14 --- .../presentation/router/DefaultWalletRouter.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 5dd19e4a33..e5ad37394c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -54,17 +54,17 @@ internal class DefaultWalletRouter( ) { composable(WalletRoute.Wallet.route) { if (walletFeatureToggles.isWalletsScrollingPreviewEnabled) { - val viewModel = hiltViewModel().apply { - router = this@DefaultWalletRouter - } - - WalletScreen(state = viewModel.uiState) - } else { val viewModel = hiltViewModel().apply { setWalletRouter(router = this@DefaultWalletRouter) } WalletScreenV2(state = viewModel.uiState.collectAsStateWithLifecycle().value) + } else { + val viewModel = hiltViewModel().apply { + router = this@DefaultWalletRouter + } + + WalletScreen(state = viewModel.uiState) } } From 44494e9ba6f0942609c8740542ac66a2f4b4b9d9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 28 Nov 2023 16:43:57 +0100 Subject: [PATCH 089/139] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 11 ++- .../core/ui/extensions/CryptoCurrency.kt | 10 +++ data/tokens/build.gradle.kts | 1 + .../tangem/data/tokens/di/TokensDataModule.kt | 24 ++++-- .../data/tokens/paging/CoinsPagingSource.kt | 77 +++++++++++++++++++ .../tokens/paging/CoinsResponseConverter.kt | 51 ++++++++++++ .../repository/DefaultTokensListRepository.kt | 45 +++++++++++ .../data/tokens/utils/NetworkOperations.kt | 2 +- domain/tokens/build.gradle.kts | 3 + .../com/tangem/domain/tokens/model/Quote.kt | 7 +- .../com/tangem/domain/tokens/model/Token.kt | 41 ++++++++++ .../tokens/GetGlobalTokenListUseCase.kt | 22 ++++++ .../tokens/repository/TokensListRepository.kt | 20 +++++ features/manage-tokens/impl/build.gradle.kts | 3 + 14 files changed, 302 insertions(+), 15 deletions(-) create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetGlobalTokenListUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index d66c81c65f..d4488fe919 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -2,10 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -231,4 +228,10 @@ internal object TokensDomainModule { ): GetMissedAddressesCryptoCurrenciesUseCase { return GetMissedAddressesCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository) } + + @Provides + @ViewModelScoped + fun provideGetGlobalTokenListUseCase(tokensListRepository: TokensListRepository): GetGlobalTokenListUseCase { + return GetGlobalTokenListUseCase(repository = tokensListRepository) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt index cf0b07bbfe..277d3f4a52 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt @@ -32,6 +32,16 @@ fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon( ): Color { if (isGrayscale) return TangemColorPalette.Dark2 + return tryGetBackgroundForTokenIcon(contractAddress = contractAddress, fallbackColor = fallbackColor) +} + +/** + * Tries to extract a background color from the contract address of a token. + * + * @param fallbackColor The color to use as a fallback. + * @return The extracted background color or the fallback color if extraction fails or if it is a test network token. + */ +fun tryGetBackgroundForTokenIcon(contractAddress: String, fallbackColor: Color = TangemColorPalette.Black): Color { return try { val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX) Color(colorHex.toColorInt()) diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 88d7471830..f183caa14f 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -45,4 +45,5 @@ dependencies { implementation(deps.jodatime) implementation(deps.timber) implementation(deps.retrofit) // For HttpException + implementation(deps.androidx.paging.runtime) } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 80ceefbdc9..f435378502 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -1,10 +1,7 @@ package com.tangem.data.tokens.di import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.tokens.repository.DefaultCurrenciesRepository -import com.tangem.data.tokens.repository.DefaultMarketCryptoCurrencyRepository -import com.tangem.data.tokens.repository.DefaultNetworksRepository -import com.tangem.data.tokens.repository.DefaultQuotesRepository +import com.tangem.data.tokens.repository.* import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -12,10 +9,7 @@ import com.tangem.datasource.local.quote.QuotesStore import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -93,4 +87,18 @@ internal object TokensDataModule { ): MarketCryptoCurrencyRepository { return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore) } + + @Provides + @Singleton + fun providesTokensListRepository( + tangemTechApi: TangemTechApi, + dispatchers: CoroutineDispatcherProvider, + quotesRepository: QuotesRepository, + ): TokensListRepository { + return DefaultTokensListRepository( + tangemTechApi = tangemTechApi, + dispatchers = dispatchers, + quotesRepository = quotesRepository, + ) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt new file mode 100644 index 0000000000..beb9f10b36 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsPagingSource.kt @@ -0,0 +1,77 @@ +package com.tangem.data.tokens.paging + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider + +/** + * A PagingSource responsible for retrieving tokens from the Tangem Tech API and adding to them quotes information. + * + * @property api Tangem Tech API + * @property quotesRepository Repository providing quotes data. + * @property dispatchers Coroutine dispatchers provider. + * @property searchText The search text used to filter tokens. + */ +internal class CoinsPagingSource( + private val api: TangemTechApi, + private val quotesRepository: QuotesRepository, + private val dispatchers: CoroutineDispatcherProvider, + private val searchText: String?, +) : PagingSource() { + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: 0 + + return com.tangem.utils.coroutines.runCatching(dispatchers.io) { + api.getCoins( + active = true, // TODO change when voting functionality is implemented + searchText = searchText, + offset = page * params.loadSize, + limit = params.loadSize, + ) + }.fold( + onSuccess = { response -> + val coinsIds = response.coins.map { coin -> + CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(coin.id), + suffix = CryptoCurrency.ID.Suffix.RawID(coin.id), + ) + } + val quotes = quotesRepository.getQuotesSync(currenciesIds = coinsIds.toSet(), refresh = false) + + LoadResult.Page( + data = CoinsResponseConverter.convert( + CoinsData( + response.coins, + response.imageHost, + quotes, + ), + ), + prevKey = if (page == 0) null else page.minus(other = 1), + nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1), + ) + }, + onFailure = { LoadResult.Error(it) }, + ) + } +} + +data class CoinsData( + val coins: List, + val imageHost: String?, + val quotes: Set, +) \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt new file mode 100644 index 0000000000..7735b24528 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/paging/CoinsResponseConverter.kt @@ -0,0 +1,51 @@ +package com.tangem.data.tokens.paging + +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.tokens.utils.getNetworkStandardType +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Token +import com.tangem.utils.converter.Converter + +/** + * Converter from data model [CoinsResponse] to list of domain models [Token] + */ +internal object CoinsResponseConverter : Converter> { + + override fun convert(value: CoinsData): List { + return value.coins.map { coin -> + val id = CryptoCurrency.ID( + CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + CryptoCurrency.ID.Body.NetworkId(coin.id), + CryptoCurrency.ID.Suffix.RawID(coin.id), + ) + val quote = value.quotes.find { it.rawCurrencyId == id.rawCurrencyId } + Token( + id = id.rawCurrencyId ?: coin.name, + name = coin.name, + symbol = coin.symbol, + iconUrl = getIconUrl(coin.id, value.imageHost), + isAvailable = coin.active, + networks = coin.networks.mapNotNull { network -> + val blockchain = Blockchain.fromNetworkId(network.networkId) ?: return@mapNotNull null + Token.Network( + networkId = network.networkId, + standardType = getNetworkStandardType(blockchain).name, + address = network.contractAddress, + iconUrl = getIconUrl(network.networkId, value.imageHost), + decimalCount = network.decimalCount?.toInt(), + ) + }, + quote = quote, + ) + } + } +} + +internal fun getIconUrl(id: String, imageHost: String? = null): String { + return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png" +} + +private const val DEFAULT_IMAGE_HOST = + "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/" \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt new file mode 100644 index 0000000000..d4f2d9a9a2 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensListRepository.kt @@ -0,0 +1,45 @@ +package com.tangem.data.tokens.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.tangem.data.tokens.paging.CoinsPagingSource +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensListRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow + +/** + * Default repository implementation for managing operations related to a complete set of tokens + * + * @property tangemTechApi Tangem Tech API + * @property dispatchers coroutine dispatchers provider + * @property quotesRepository responsible for providing cryptocurrency quotes data + * + */ +internal class DefaultTokensListRepository( + private val tangemTechApi: TangemTechApi, + private val dispatchers: CoroutineDispatcherProvider, + private val quotesRepository: QuotesRepository, +) : TokensListRepository { + + override fun getTokens(searchText: String?): Flow> { + return Pager( + config = PagingConfig( + pageSize = 100, + prefetchDistance = 70, + enablePlaceholders = false, + ), + pagingSourceFactory = { + CoinsPagingSource( + api = tangemTechApi, + dispatchers = dispatchers, + searchText = searchText, + quotesRepository = quotesRepository, + ) + }, + ).flow + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt index 5e87555133..21ef628101 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt @@ -50,7 +50,7 @@ private fun getNetworkDerivationPath( } } -private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType { +internal fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType { return when (blockchain) { Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20 Blockchain.BSC, Blockchain.BSCTestnet -> Network.StandardType.BEP20 diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index c146658684..8fabb669f0 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -22,6 +22,9 @@ dependencies { /** Project - Other */ implementation(projects.core.utils) + /** Android - Other */ + implementation(deps.androidx.paging.runtime) + /** Utils */ implementation(deps.jodatime) implementation(deps.reKotlin) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt index 899574d5e4..9f5dfbe387 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt @@ -3,14 +3,17 @@ package com.tangem.domain.tokens.model import java.math.BigDecimal /** - * Represents a financial quote for a specific cryptocurrency, including its fiat exchange rate and price change. + * Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change. * - * @property rawCurrencyId The unique identifier of the token for which the quote is provided. + * @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided. * @property fiatRate The current fiat exchange rate for the cryptocurrency. * @property priceChange The price change for the cryptocurrency. + * @property values The values representing the cryptocurrency's price changes over a 24-hour period, + * suitable for chart plotting. */ data class Quote( val rawCurrencyId: String, val fiatRate: BigDecimal, val priceChange: BigDecimal, + val values: List? = null, ) \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt new file mode 100644 index 0000000000..ed26e356b8 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.tokens.model + +/** + * Represents a domain model for a token (as part of a general list of tokens). + * + * @property id The unique identifier of the token. + * @property networks List of networks associated with the token. + * @property isAvailable Indicates whether this token is supported in Tangem app. + * @property quote Equivalent prices for the token. + * @property name The name of the token. + * @property symbol The brief name of the token, e.g., "BTC". + * @property iconUrl URL of the token's icon. + */ +data class Token( + val id: String, + val networks: List, + val isAvailable: Boolean, + val quote: Quote?, + val name: String, + val symbol: String, + val iconUrl: String, +) { + + /** + * Represents a domain model for a network associated with a token. + * + * @property networkId The unique identifier of the network. + * @property standardType The type of blockchain associated with the network. + * @property address The contract address of the token on the current network. + * It is null for the main currencies of the network. + * @property iconUrl URL of the network's icon. + * @property decimalCount The decimal count associated with the token on the network. + */ + data class Network( + val networkId: String, + val standardType: String, + val address: String?, + val iconUrl: String, + val decimalCount: Int?, + ) +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetGlobalTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetGlobalTokenListUseCase.kt new file mode 100644 index 0000000000..74ddd5b4e4 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetGlobalTokenListUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.tokens + +import androidx.paging.PagingData +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.TokensListRepository +import kotlinx.coroutines.flow.Flow + +/** + * Use case for retrieving a complete set of tokens with their quotes (filtered by a search text if provided). + * + * @property repository The repository responsible for fetching and preparing the list of tokens with their quotes. + */ +class GetGlobalTokenListUseCase(private val repository: TokensListRepository) { + + /** + * @param searchText The text used for filtering the list of tokens (not required). + * @return A [Flow] emitting [PagingData] containing the tokens with their quotes. + */ + operator fun invoke(searchText: String?): Flow> { + return repository.getTokens(searchText = searchText?.ifBlank(defaultValue = { null })) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt new file mode 100644 index 0000000000..96db81b6af --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensListRepository.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.tokens.repository + +import androidx.paging.PagingData +import com.tangem.domain.tokens.model.Token +import kotlinx.coroutines.flow.Flow + +/** + * Repository interface for managing operations related to a complete set of tokens + * (not associated with any specific card). + * */ +interface TokensListRepository { + + /** + * Retrieves a list of available tokens with quotes, can be filtered based on the provided search text. + * + * @param searchText The search text used to filter tokens. + * @return A [Flow] emitting [PagingData] containing the tokens with quotes matching the search criteria. + */ + fun getTokens(searchText: String?): Flow> +} \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 76bd921f71..fab77971a3 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -47,6 +47,9 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.utils) + /** Project - Data */ + implementation(projects.data.tokens) + /** Domain modules */ implementation(projects.common) implementation(projects.domain.card) From 84390ebc91b8a5608401f7592f128cf1826ec184 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 17:10:11 +0300 Subject: [PATCH 090/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 9 +- .../swap/ui/ChooseProviderBottomSheet.kt | 16 ++-- .../tangem/feature/swap/ui/ProviderItem.kt | 2 +- .../tangem/feature/swap/ui/StateBuilder.kt | 40 ++++---- .../feature/swap/ui/SwapSelectTokenScreen.kt | 4 +- .../feature/swap/viewmodels/SwapViewModel.kt | 91 +++++++++++++------ 6 files changed, 99 insertions(+), 63 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 31615ff878..5a67ab7227 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -302,7 +302,7 @@ internal class SwapInteractorImpl @Inject constructor( val fromTokenAddress = getTokenAddress(fromToken.currency) val isAllowedToSpend = quotes.dataModel?.allowanceContract?.let { isAllowedToSpend(networkId, fromToken.currency, amount, it) - } ?: false + } ?: true if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) @@ -543,19 +543,18 @@ internal class SwapInteractorImpl @Inject constructor( value = fee.feeValue, currencySymbol = fee.cryptoSymbol, decimals = fee.decimals, - type = AmountType.Coin + type = AmountType.Coin, ) return if (fee.gasLimit != 0) { Fee.Ethereum( amount = feeAmount, gasLimit = fee.gasLimit.toBigInteger(), - gasPrice = (feeAmountValue / fee.gasLimit.toBigDecimal()).toBigInteger() + gasPrice = (feeAmountValue / fee.gasLimit.toBigDecimal()).toBigInteger(), ) } else { Fee.Common(feeAmount) } - } @Deprecated("used in old swap mechanism") @@ -1083,7 +1082,7 @@ internal class SwapInteractorImpl @Inject constructor( val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = feeNormal, - decimals = decimals + decimals = decimals, ) val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = feePriority, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index f11e395923..30954893a7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -2,7 +2,6 @@ package com.tangem.feature.swap.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.Text @@ -24,16 +23,17 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content: ChooseProviderBottomSheetConfig -> + TangemBottomSheet( + config = config, + color = TangemTheme.colors.background.tertiary, + ) { content: ChooseProviderBottomSheetConfig -> ChooseProviderBottomSheetContent(content = content) } } @Composable private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetConfig) { - Column( - modifier = Modifier.background(TangemTheme.colors.background.primary), - ) { + Column { Text( text = stringResource(R.string.express_choose_providers_title), style = TangemTheme.typography.subtitle1, @@ -60,7 +60,6 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC shape = TangemTheme.shapes.roundedCornersXMedium, ) .clip(shape = TangemTheme.shapes.roundedCornersXMedium), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { content.providers.forEach { provider -> val isSelected = provider.id == content.selectedProviderId @@ -72,10 +71,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC enabled = provider.onProviderClick != null, onClick = { provider.onProviderClick?.invoke(provider.id) }, ) - .padding( - horizontal = TangemTheme.dimens.spacing12, - vertical = TangemTheme.dimens.spacing12, - ), + .padding(TangemTheme.dimens.spacing12), ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 99595d6398..550fbfcad6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -146,7 +146,7 @@ private fun ProviderContentState( ) if (state.percentLowerThenBest != null) { Text( - text = "${state.percentLowerThenBest}%", + text = "-${state.percentLowerThenBest}%", style = TangemTheme.typography.body2, color = TangemTheme.colors.text.warning, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index a13a37110c..0b18d866ad 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -194,12 +194,13 @@ internal class StateBuilder( * @param fromToken token data to swap * @return updated whole screen state */ - @Suppress("LongMethod") + @Suppress("LongMethod", "LongParameterList") fun createQuotesLoadedState( uiStateHolder: SwapStateHolder, quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, swapProvider: SwapProvider, + bestRatedProviderId: String, selectedFeeType: FeeType, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -288,6 +289,7 @@ internal class StateBuilder( ), updateInProgress = false, providerState = swapProvider.convertToContentClickableProviderState( + isBestRate = bestRatedProviderId == swapProvider.providerId, fromTokenInfo = quoteModel.fromTokenInfo, toTokenInfo = quoteModel.toTokenInfo, selectionType = ProviderState.SelectionType.CLICK, @@ -310,6 +312,7 @@ internal class StateBuilder( swapProvider = swapProvider, fromToken = fromToken.cryptoCurrencyStatus.currency, dataError = dataError, + onProviderClick = actions.onProviderClick, selectionType = ProviderState.SelectionType.CLICK, ) val receiveCardData = toToken?.let { @@ -358,6 +361,7 @@ internal class StateBuilder( swapProvider: SwapProvider, fromToken: CryptoCurrency, dataError: DataError, + onProviderClick: (String) -> Unit, selectionType: ProviderState.SelectionType, ): ProviderState { return when (dataError) { @@ -368,7 +372,7 @@ internal class StateBuilder( wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), ), selectionType = selectionType, - onProviderClick = actions.onProviderClick, + onProviderClick = onProviderClick, ) } else -> { @@ -712,13 +716,13 @@ internal class StateBuilder( fun showSelectProviderBottomSheet( uiState: SwapStateHolder, selectedProviderId: String, - bestRatedProviderId: String, + pricesLowerBest: Map, providersStates: Map, unavailableProviders: List, onDismiss: () -> Unit, ): SwapStateHolder { val availableProvidersStates = providersStates.entries.mapNotNull { - it.convertToProviderState(bestRatedProviderId, actions.onProviderSelect) + it.convertToProviderBottomSheetState(pricesLowerBest, actions.onProviderSelect) } val unavailableProviderStates = unavailableProviders.map { it.convertToUnavailableProviderState( @@ -818,23 +822,25 @@ internal class StateBuilder( ).toImmutableList() } - private fun Map.Entry.convertToProviderState( - bestRatedProviderId: String, + private fun Map.Entry.convertToProviderBottomSheetState( + pricesLowerBest: Map, onProviderSelect: (String) -> Unit, ): ProviderState? { val provider = this.key return when (val state = this.value) { is SwapState.EmptyAmountState -> null is SwapState.QuotesLoadedState -> provider.convertToContentSelectableProviderState( - isBestRate = provider.providerId == bestRatedProviderId, + isBestRate = false, // not show best rate in bottom sheet state = state, onProviderClick = onProviderSelect, + pricesLowerBest = pricesLowerBest, selectionType = ProviderState.SelectionType.SELECT, ) is SwapState.SwapError -> getProviderStateForError( swapProvider = provider, fromToken = state.fromTokenInfo.cryptoCurrencyStatus.currency, dataError = state.error, + onProviderClick = onProviderSelect, selectionType = ProviderState.SelectionType.SELECT, ) } @@ -893,6 +899,7 @@ internal class StateBuilder( } private fun SwapProvider.convertToContentClickableProviderState( + isBestRate: Boolean, fromTokenInfo: TokenSwapInfo, toTokenInfo: TokenSwapInfo, selectionType: ProviderState.SelectionType, @@ -905,13 +912,18 @@ internal class StateBuilder( val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" + val badge = if (isBestRate) { + ProviderState.AdditionalBadge.BestTrade + } else { + ProviderState.AdditionalBadge.Empty + } return ProviderState.Content( id = this.providerId, name = this.name, iconUrl = this.imageLarge, type = this.type.toString(), rate = rateString, - additionalBadge = ProviderState.AdditionalBadge.BestTrade, + additionalBadge = badge, selectionType = selectionType, percentLowerThenBest = null, onProviderClick = onProviderClick, @@ -922,17 +934,11 @@ internal class StateBuilder( isBestRate: Boolean, state: SwapState.QuotesLoadedState, selectionType: ProviderState.SelectionType, + pricesLowerBest: Map, onProviderClick: (String) -> Unit, ): ProviderState { - val fromTokenInfo = state.fromTokenInfo val toTokenInfo = state.toTokenInfo - val rate = toTokenInfo.tokenAmount.value.calculateRate( - fromTokenInfo.tokenAmount.value, - toTokenInfo.cryptoCurrencyStatus.currency.decimals, - ) - val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol - val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol - val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" + val rateString = toTokenInfo.tokenAmount.getFormattedCryptoAmount(toTokenInfo.cryptoCurrencyStatus.currency) val additionalBadge = if (state.permissionState is PermissionDataState.PermissionReadyForRequest) { ProviderState.AdditionalBadge.PermissionRequired } else if (isBestRate) { @@ -948,7 +954,7 @@ internal class StateBuilder( rate = rateString, additionalBadge = additionalBadge, selectionType = selectionType, - percentLowerThenBest = null, + percentLowerThenBest = pricesLowerBest[this], onProviderClick = onProviderClick, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 9d90e20505..e1e6a80d73 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -63,7 +63,7 @@ private fun EmptyTokensList(modifier: Modifier = Modifier) { Box( modifier = modifier .background(TangemTheme.colors.background.secondary) - .fillMaxSize() + .fillMaxSize(), ) { Column(modifier = Modifier.align(Alignment.Center)) { Image( @@ -82,7 +82,7 @@ private fun EmptyTokensList(modifier: Modifier = Modifier) { text = stringResource(id = R.string.exchange_tokens_empty_tokens), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index d1dcdef1c0..9304f38196 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import arrow.core.getOrElse +import arrow.core.mapNotNull import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter @@ -42,6 +43,8 @@ import java.util.Locale import javax.inject.Inject import kotlin.properties.Delegates +typealias SuccessLoadedSwapData = Map + @Suppress("LargeClass", "LongParameterList") @HiltViewModel internal class SwapViewModel @Inject constructor( @@ -257,8 +260,12 @@ internal class SwapViewModel @Inject constructor( } }, onSuccess = { providersState -> - val (provider, state) = updateLoadedQuotes(providersState) - setupLoadedState(provider, state, fromToken) + if (providersState.isNotEmpty()) { + val (provider, state) = updateLoadedQuotes(providersState) + setupLoadedState(provider, state, fromToken) + } else { + Timber.e("Accidentally empty quotes list") + } }, onError = { Timber.e("Error when loading quotes: $it") @@ -271,11 +278,14 @@ internal class SwapViewModel @Inject constructor( when (state) { is SwapState.QuotesLoadedState -> { fillLoadedDataState(state, state.permissionState, state.swapDataModel) + val loadedStates = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() + val bestRatedProviderId = findBestQuoteProvider(loadedStates)?.providerId ?: provider.providerId uiState = stateBuilder.createQuotesLoadedState( uiStateHolder = uiState, quoteModel = state, fromToken = fromToken.currency, swapProvider = provider, + bestRatedProviderId = bestRatedProviderId, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) } @@ -309,15 +319,13 @@ internal class SwapViewModel @Inject constructor( lastLoadedSwapStates = state, ) selectedSwapProvider?.let { - return state.entries.first { it.key == selectedSwapProvider }.toPair() + return nonEmptyStates.entries.first { it.key == selectedSwapProvider }.toPair() } return state.entries.first().toPair() } private fun selectProvider(state: Map): SwapProvider { - val stateSuccess = state - .filter { it.value is SwapState.QuotesLoadedState } - .mapValues { it.value as SwapState.QuotesLoadedState } + val stateSuccess = state.getLastLoadedSuccessStates() return if (stateSuccess.isNotEmpty()) { val currentSelected = dataState.selectedProvider if (currentSelected != null && state.keys.contains(currentSelected)) { @@ -330,23 +338,6 @@ internal class SwapViewModel @Inject constructor( } } - private fun findBestQuoteProvider(state: Map): SwapProvider? { - // finding best quotes - return state.mapValues { - if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && - !it.value.toTokenInfo.amountFiat.isNullOrZero() - ) { - it.value.fromTokenInfo.amountFiat.divide( - it.value.toTokenInfo.amountFiat, - it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals, - RoundingMode.HALF_UP, - ) - } else { - BigDecimal.ZERO - } - }.minByOrNull { it.value }?.key - } - private fun fillLoadedDataState( state: SwapState.QuotesLoadedState, permissionState: PermissionDataState, @@ -714,15 +705,13 @@ internal class SwapViewModel @Inject constructor( } }, onProviderClick = { providerId -> - val states = dataState.lastLoadedSwapStates - .filter { it.value is SwapState.QuotesLoadedState } - .mapValues { it.value as SwapState.QuotesLoadedState } - val bestRatedProviderId = findBestQuoteProvider(states)?.providerId ?: providerId + val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() + val pricesLowerBest = getPricesLowerBest(states) val unavailableProviders = getUnavailableProvidersFor(dataState.lastLoadedSwapStates) uiState = stateBuilder.showSelectProviderBottomSheet( uiState = uiState, selectedProviderId = providerId, - bestRatedProviderId = bestRatedProviderId, + pricesLowerBest = pricesLowerBest, unavailableProviders = unavailableProviders, providersStates = dataState.lastLoadedSwapStates, ) { uiState = stateBuilder.dismissBottomSheet(uiState) } @@ -754,6 +743,45 @@ internal class SwapViewModel @Inject constructor( return selectedProvider } + private fun findBestQuoteProvider(state: SuccessLoadedSwapData): SwapProvider? { + // finding best quotes + return state.minByOrNull { + if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && + !it.value.toTokenInfo.amountFiat.isNullOrZero() + ) { + it.value.fromTokenInfo.amountFiat.divide( + it.value.toTokenInfo.amountFiat, + it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals, + RoundingMode.HALF_UP, + ) + } else { + BigDecimal.ZERO + } + }?.key + } + + private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map { + val rates = state.mapValues { + it.value.fromTokenInfo.amountFiat.divide( + it.value.toTokenInfo.amountFiat, + 2, + RoundingMode.HALF_UP, + ) + } + val bestRate = rates.minByOrNull { it.value } ?: return emptyMap() + return rates.mapNotNull { + if (it.key != bestRate.key) { + val percentDiff = bestRate.value + .divide(it.value, 2, RoundingMode.HALF_UP) + .multiply(BigDecimal(HUNDRED_PERCENT)) + .toFloat() + HUNDRED_PERCENT - percentDiff + } else { + null + } + } + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> @@ -794,10 +822,17 @@ internal class SwapViewModel @Inject constructor( return getAllProviders().filterNot { it in state } } + private fun Map.getLastLoadedSuccessStates(): SuccessLoadedSwapData { + return this + .filter { it.value is SwapState.QuotesLoadedState } + .mapValues { it.value as SwapState.QuotesLoadedState } + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" private const val UPDATE_DELAY = 10000L private const val DEBOUNCE_AMOUNT_DELAY = 1000L + private const val HUNDRED_PERCENT = 100 } } \ No newline at end of file From 4c23520e0851b819885f76e16a6e5b91403c26c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 17:26:29 +0300 Subject: [PATCH 091/139] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../feature/swap/domain/SwapInteractor.kt | 13 +-- .../feature/swap/domain/SwapInteractorImpl.kt | 92 ++++--------------- .../swap/domain/cache/SwapDataCache.kt | 15 --- .../swap/domain/cache/SwapDataCacheImpl.kt | 41 --------- .../swap/domain/di/SwapDomainModule.kt | 2 - .../feature/swap/viewmodels/SwapViewModel.kt | 2 +- 7 files changed, 21 insertions(+), 146 deletions(-) delete mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt delete mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 2a612cd92b..6ef8ce45d1 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 2a612cd92b1c78b99c917d0fe66342831f801e5e +Subproject commit 6ef8ce45d183905b5752e2d33c1d8bf2f4bcace6 diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 6f550682f4..01193a0ea4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -14,16 +14,6 @@ interface SwapInteractor { fun initDerivationPathAndNetwork(derivationPath: String?, network: Network) - /** - * On search token, locally search tokens in previously loaded list to swap - * searching in names and symbols - * - * @param networkId networkId for tokens - * @param searchQuery string query for search - * @return [FoundTokensStateExpress] that contains list of tokens matching condition query - */ - suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensStateExpress - /** * Gives permission to swap, this starts scan card process * @@ -88,10 +78,9 @@ interface SwapInteractor { /** * Returns token in wallet balance * - * @param networkId * @param token */ - fun getTokenBalance(networkId: String, token: CryptoCurrency): SwapAmount + fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount fun isAvailableToSwap(networkId: String): Boolean diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 5a67ab7227..a97c761008 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -19,7 +19,6 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.feature.swap.domain.cache.SwapDataCache import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel @@ -44,7 +43,6 @@ internal class SwapInteractorImpl @Inject constructor( private val transactionManager: TransactionManager, private val userWalletManager: UserWalletManager, private val repository: SwapRepository, - private val cache: SwapDataCache, private val allowPermissionsHandler: AllowPermissionsHandler, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, @@ -166,25 +164,6 @@ internal class SwapInteractorImpl @Inject constructor( this.network = network } - @Deprecated("used in old swap mechanism") - override suspend fun searchTokens(networkId: String, searchQuery: String): FoundTokensStateExpress { - val searchQueryLowerCase = searchQuery.lowercase() - val tokensInWallet = cache.getInWalletTokens() - .filter { - it.token.name.lowercase().contains(searchQueryLowerCase) || - it.token.symbol.lowercase().contains(searchQueryLowerCase) - } - val loadedTokens = cache.getLoadedTokens() - .filter { - it.token.name.lowercase().contains(searchQueryLowerCase) || - it.token.symbol.lowercase().contains(searchQueryLowerCase) - } - return FoundTokensStateExpress( - tokensInWallet = tokensInWallet, - loadedTokens = loadedTokens, - ) - } - @Deprecated("used in old swap mechanism") override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState { val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) { @@ -238,19 +217,14 @@ internal class SwapInteractorImpl @Inject constructor( selectedFee: FeeType, ): Map { return providers.map { provider -> - syncWalletBalanceForTokens(networkId, listOf(fromToken.currency, toToken.currency)) val amountDecimal = toBigDecimalOrNull(amountToSwap) if (amountDecimal == null || amountDecimal.signum() == 0) { return providers.associateWith { - createEmptyAmountState( - networkId, - fromToken.currency, - toToken.currency, - ) + createEmptyAmountState(fromToken, toToken) } } val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) - val isBalanceWithoutFeeEnough = isBalanceEnough(networkId, fromToken.currency, amount, null) + val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null) when (provider.type) { ExchangeProviderType.DEX -> { @@ -406,7 +380,7 @@ internal class SwapInteractorImpl @Inject constructor( val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = state.txFee) val isBalanceIncludeFeeEnough = - isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority) + isBalanceEnough(fromToken, amount, feeByPriority) val isFeeEnough = checkFeeIsEnough( fee = feeByPriority, spendAmount = amount, @@ -558,12 +532,8 @@ internal class SwapInteractorImpl @Inject constructor( } @Deprecated("used in old swap mechanism") - override fun getTokenBalance(networkId: String, token: CryptoCurrency): SwapAmount { - return cache.getBalanceForToken( - networkId = networkId, - derivationPath = derivationPath, - symbol = token.symbol, - ) ?: SwapAmount(BigDecimal.ZERO, getTokenDecimals(token)) + override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount { + return SwapAmount(token.value.amount ?: BigDecimal.ZERO, getTokenDecimals(token.currency)) } @Deprecated("used in old swap mechanism") @@ -610,17 +580,13 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private fun createEmptyAmountState( - networkId: String, - fromToken: CryptoCurrency, - toToken: CryptoCurrency, - ): SwapState { + private fun createEmptyAmountState(fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus): SwapState { val appCurrency = userWalletManager.getUserAppCurrency() - val fromTokenBalance = cache.getBalanceForToken(networkId, derivationPath, fromToken.symbol) - val toTokenBalance = cache.getBalanceForToken(networkId, derivationPath, toToken.symbol) + val fromTokenBalance = getTokenBalance(fromToken) + val toTokenBalance = getTokenBalance(toToken) return SwapState.EmptyAmountState( - fromTokenWalletBalance = fromTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(), - toTokenWalletBalance = toTokenBalance?.let { amountFormatter.formatSwapAmountToUI(it, "") }.orEmpty(), + fromTokenWalletBalance = amountFormatter.formatSwapAmountToUI(fromTokenBalance, ""), + toTokenWalletBalance = amountFormatter.formatSwapAmountToUI(toTokenBalance, ""), zeroAmountEquivalent = BigDecimal.ZERO.toFiatString( rateValue = BigDecimal.ONE, fiatCurrencyName = appCurrency.symbol, @@ -706,7 +672,7 @@ internal class SwapInteractorImpl @Inject constructor( ExchangeProviderType.DEX -> { val state = updatePermissionState( networkId = networkId, - fromToken = fromToken.currency, + fromTokenStatus = fromToken, swapAmount = amount, quotesLoadedState = swapState, isAllowedToSpend = isAllowedToSpend, @@ -799,8 +765,7 @@ internal class SwapInteractorImpl @Inject constructor( is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(networkId) } val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState) - val isBalanceIncludeFeeEnough = - isBalanceEnough(networkId, fromToken.currency, amount, feeByPriority) + val isBalanceIncludeFeeEnough = isBalanceEnough(fromToken, amount, feeByPriority) val isFeeEnough = checkFeeIsEnough( fee = feeByPriority, spendAmount = amount, @@ -908,19 +873,20 @@ internal class SwapInteractorImpl @Inject constructor( @Suppress("LongParameterList", "LongMethod") private suspend fun updatePermissionState( networkId: String, - fromToken: CryptoCurrency, + fromTokenStatus: CryptoCurrencyStatus, swapAmount: SwapAmount, quotesLoadedState: SwapState.QuotesLoadedState, spenderAddress: String, isAllowedToSpend: Boolean, ): SwapState.QuotesLoadedState { + val fromToken = fromTokenStatus.currency if (isAllowedToSpend) { return quotesLoadedState.copy( permissionState = PermissionDataState.Empty, ) } // if token balance ZERO not show permission state to avoid user to spend money for fee - val isTokenZeroBalance = getTokenBalance(networkId, fromToken).value.signum() == 0 + val isTokenZeroBalance = getTokenBalance(fromTokenStatus).value.signum() == 0 if (isTokenZeroBalance) { return quotesLoadedState.copy( permissionState = PermissionDataState.Empty, @@ -989,23 +955,6 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun syncWalletBalanceForTokens(networkId: String, tokens: List) { - val tokensToSync = tokens.filter { cache.getBalanceForToken(networkId, derivationPath, it.symbol) == null } - if (tokensToSync.isNotEmpty()) { - val tokensBalance = - userWalletManager.getCurrentWalletTokensBalance( - networkId = networkId, - extraTokens = tokensToSync.map { swapCurrencyConverter.convert(it) }, - derivationPath = derivationPath, - ) - cache.cacheBalances( - networkId = networkId, - derivationPath = derivationPath, - balances = tokensBalance.mapValues { SwapAmount(it.value.value, it.value.decimals) }, - ) - } - } - private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState(networkId: String): TxFeeState { val normalFeeValue = this.minFee.fee.value // in swap for normal use min fee val normalFeeGas = this.minFee.gasLimit.toInt() @@ -1149,14 +1098,9 @@ internal class SwapInteractorImpl @Inject constructor( } } - private fun isBalanceEnough( - networkId: String, - fromToken: CryptoCurrency, - amount: SwapAmount, - fee: BigDecimal?, - ): Boolean { - val tokenBalance = getTokenBalance(networkId, fromToken).value - return if (fromToken is CryptoCurrency.Token) { + private fun isBalanceEnough(fromToken: CryptoCurrencyStatus, amount: SwapAmount, fee: BigDecimal?): Boolean { + val tokenBalance = getTokenBalance(fromToken).value + return if (fromToken.currency is CryptoCurrency.Token) { tokenBalance >= amount.value } else { tokenBalance > amount.value.plus(fee ?: BigDecimal.ZERO) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt deleted file mode 100644 index ce4956ba90..0000000000 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCache.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.feature.swap.domain.cache - -import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress - -interface SwapDataCache { - - fun cacheInWalletTokens(tokens: List) - fun cacheLoadedTokens(tokens: List) - fun cacheBalances(networkId: String, derivationPath: String?, balances: Map) - - fun getInWalletTokens(): List - fun getLoadedTokens(): List - fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? -} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt deleted file mode 100644 index 92c051e246..0000000000 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/cache/SwapDataCacheImpl.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.feature.swap.domain.cache - -import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.ui.TokenWithBalanceExpress - -class SwapDataCacheImpl : SwapDataCache { - - private val tokensBalances: MutableMap> = mutableMapOf() - private val lastInWalletTokens = mutableListOf() - private val lastLoadedTokens = mutableListOf() - - override fun cacheInWalletTokens(tokens: List) { - lastInWalletTokens.clear() - lastInWalletTokens.addAll(tokens) - } - - override fun cacheLoadedTokens(tokens: List) { - lastLoadedTokens.clear() - lastLoadedTokens.addAll(tokens) - } - - override fun getInWalletTokens(): List { - return lastInWalletTokens - } - - override fun getLoadedTokens(): List { - return lastLoadedTokens - } - - override fun getBalanceForToken(networkId: String, derivationPath: String?, symbol: String): SwapAmount? { - return tokensBalances[createKeyFrom(networkId, derivationPath)]?.get(symbol) - } - - override fun cacheBalances(networkId: String, derivationPath: String?, balances: Map) { - tokensBalances[createKeyFrom(networkId, derivationPath)] = balances - } - - private fun createKeyFrom(networkId: String, derivationPath: String?): String { - return "$networkId;$derivationPath" - } -} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index dd81f44d66..e161ee2429 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -13,7 +13,6 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* -import com.tangem.feature.swap.domain.cache.SwapDataCacheImpl import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -45,7 +44,6 @@ class SwapDomainModule { transactionManager = transactionManager, userWalletManager = userWalletManager, repository = swapRepository, - cache = SwapDataCacheImpl(), allowPermissionsHandler = AllowPermissionsHandlerImpl(), getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 9304f38196..4122723684 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -592,7 +592,7 @@ internal class SwapViewModel @Inject constructor( private fun onMaxAmountClicked() { dataState.fromCryptoCurrency?.let { - val balance = swapInteractor.getTokenBalance(initialCryptoCurrency.network.id.value, it.currency) + val balance = swapInteractor.getTokenBalance(it) onAmountChanged(balance.formatToUIRepresentation()) } } From f3aa08093d89285de57306382d89de0f62a0567b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 15:51:25 +0300 Subject: [PATCH 092/139] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 2 +- .../ui/components/inputrow/InputRowApprox.kt | 30 ++- .../core/ui/extensions/TextReference.kt | 10 + .../tangem/utils/coroutines}/PeriodicTask.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 31 +++ .../swap/domain/di/SwapDomainModule.kt | 5 +- .../domain/SavedSwapTransactionListModel.kt | 8 +- .../swap/models/SwapSuccessStateHolder.kt | 1 + .../tangem/feature/swap/ui/StateBuilder.kt | 2 + .../feature/swap/ui/SwapSuccessScreen.kt | 29 ++- features/tokendetails/impl/build.gradle.kts | 4 + .../state/SwapTransactionsState.kt | 15 +- .../state/factory/TokenDetailsStateFactory.kt | 14 ++ ...enDetailsSwapTransactionsStateConverter.kt | 227 ++++++++++++++++++ .../components/exchange/ExchangeEstimate.kt | 4 + .../exchange/ExchangeStatusBlock.kt | 43 +--- .../exchange/ExchangeStatusBottomSheet.kt | 25 +- .../exchange/ExchangeStatusItems.kt | 81 +++++-- .../viewmodels/ExchangeStatusFactory.kt | 141 +++++++++++ .../viewmodels/TokenDetailsClickIntents.kt | 4 + .../viewmodels/TokenDetailsViewModel.kt | 108 ++++++--- 21 files changed, 657 insertions(+), 129 deletions(-) rename {features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels => core/utils/src/main/java/com/tangem/utils/coroutines}/PeriodicTask.kt (96%) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index be887dc4ea..62304b3e12 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -201,7 +201,7 @@ Visit provider’s website to see why Operation failed by provider Visit provider’s website for verification - Provider: Verification needed + KYC verification required by provider Confirmed Confirming Exchanged diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt index bd69b6feb9..b1c49c1291 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.components.inputrow.inner.DividerContainer @@ -39,6 +40,8 @@ fun InputRowApprox( rightTitle: TextReference, rightSubtitle: TextReference, modifier: Modifier = Modifier, + leftTitleEllipsisOffset: Int = 0, + rightTitleEllipsisOffset: Int = 0, showDivider: Boolean = false, ) { DividerContainer( @@ -47,7 +50,6 @@ fun InputRowApprox( ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier .padding(TangemTheme.dimens.spacing12) .fillMaxWidth(), @@ -56,6 +58,7 @@ fun InputRowApprox( iconState = leftIcon, title = leftTitle, subtitle = leftSubtitle, + titleEllipsisOffset = leftTitleEllipsisOffset, modifier = Modifier.weight(1f), ) Icon( @@ -64,7 +67,7 @@ fun InputRowApprox( tint = TangemTheme.colors.text.tertiary, modifier = Modifier .padding( - horizontal = TangemTheme.dimens.spacing8, + horizontal = TangemTheme.dimens.spacing4, vertical = TangemTheme.dimens.spacing10, ), ) @@ -72,6 +75,7 @@ fun InputRowApprox( iconState = rightIcon, title = rightTitle, subtitle = rightSubtitle, + titleEllipsisOffset = rightTitleEllipsisOffset, modifier = Modifier.weight(1f), ) } @@ -84,6 +88,7 @@ private fun InputRowApproxItem( title: TextReference, subtitle: TextReference, modifier: Modifier = Modifier, + titleEllipsisOffset: Int = 0, ) { Row( modifier = modifier, @@ -103,6 +108,7 @@ private fun InputRowApproxItem( text = title.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + ellipsis = TextEllipsis.OffsetEnd(titleEllipsisOffset), ) EllipsisText( text = subtitle.resolveReference(), @@ -122,11 +128,13 @@ private fun InputRowApproxPreview_Light() { TangemTheme { InputRowApprox( leftIcon = TokenIconState.Loading, - leftTitle = TextReference.Str("Left title"), - leftSubtitle = TextReference.Str("Left subtitle"), + leftTitle = TextReference.Str("Left title USD"), + leftSubtitle = TextReference.Str("Left subtitle USD"), + leftTitleEllipsisOffset = 3, rightIcon = TokenIconState.Loading, - rightTitle = TextReference.Str("Right title Right title Right title Right title Right title"), - rightSubtitle = TextReference.Str("Right subtitle"), + rightTitle = TextReference.Str("Right title Right title Right title Right title Right title USD"), + rightSubtitle = TextReference.Str("Right subtitle Right subtitle Right subtitle USD"), + rightTitleEllipsisOffset = 3, modifier = Modifier .background(TangemTheme.colors.background.action), ) @@ -139,11 +147,13 @@ private fun InputRowApproxPreview_Dark() { TangemTheme(isDark = true) { InputRowApprox( leftIcon = TokenIconState.Loading, - leftTitle = TextReference.Str("Left title Left title Left title Left title Left title"), - leftSubtitle = TextReference.Str("Left subtitle"), + leftTitle = TextReference.Str("Left title Left title Left title Left title Left title USD"), + leftSubtitle = TextReference.Str("Left subtitle Left subtitle Left subtitle USD"), + leftTitleEllipsisOffset = 3, rightIcon = TokenIconState.Loading, - rightTitle = TextReference.Str("Right title"), - rightSubtitle = TextReference.Str("Right subtitle"), + rightTitle = TextReference.Str("Right title USD"), + rightSubtitle = TextReference.Str("Right subtitle USD"), + rightTitleEllipsisOffset = 3, modifier = Modifier .background(TangemTheme.colors.background.action), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index d44b32ecdf..766815d328 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -104,6 +104,16 @@ fun combinedReference(refs: WrappedList): TextReference { return TextReference.Combined(refs) } +/** + * Combines multiple [TextReference] instances into a single [TextReference]. + * + * @param refs Vararg of [TextReference] instances to be combined. + * @return A [TextReference] representing the combined text references. + */ +fun combinedReference(vararg refs: TextReference): TextReference { + return TextReference.Combined(WrappedList(listOf(*refs))) +} + /** Resolve [TextReference] as [String] */ @Composable @ReadOnlyComposable diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/PeriodicTask.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt similarity index 96% rename from features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/PeriodicTask.kt rename to core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt index e1343a73cd..5db470b916 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/PeriodicTask.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/PeriodicTask.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.swap.viewmodels +package com.tangem.utils.coroutines import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index a97c761008..b4e0208243 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -43,6 +43,7 @@ internal class SwapInteractorImpl @Inject constructor( private val transactionManager: TransactionManager, private val userWalletManager: UserWalletManager, private val repository: SwapRepository, + private val swapTransactionRepository: SwapTransactionRepository, private val allowPermissionsHandler: AllowPermissionsHandler, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, @@ -492,6 +493,14 @@ internal class SwapInteractorImpl @Inject constructor( }, ifRight = { val timestamp = System.currentTimeMillis() + storeSwapTransaction( + currencyToSend = currencyToSend, + currencyToGet = currencyToGet, + amount = amount, + swapProvider = swapProvider, + swapDataModel = exchangeData.dataModel, + timestamp = timestamp, + ) TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, @@ -531,6 +540,28 @@ internal class SwapInteractorImpl @Inject constructor( } } + private suspend fun storeSwapTransaction( + currencyToSend: CryptoCurrencyStatus, + currencyToGet: CryptoCurrencyStatus, + amount: SwapAmount, + swapProvider: SwapProvider, + swapDataModel: SwapDataModel, + timestamp: Long, + ) { + swapTransactionRepository.storeTransaction( + userWalletId = UserWalletId(userWalletManager.getWalletId()), + fromCryptoCurrencyId = currencyToSend.currency.id, + toCryptoCurrencyId = currencyToGet.currency.id, + transaction = SavedSwapTransactionModel( + txId = swapDataModel.transaction.txId, + provider = swapProvider, + timestamp = timestamp, + fromCryptoAmount = amount.value, + toCryptoAmount = swapDataModel.toTokenAmount.value, + ), + ) + } + @Deprecated("used in old swap mechanism") override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount { return SwapAmount(token.value.amount ?: BigDecimal.ZERO, getTokenDecimals(token.currency)) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index e161ee2429..d72ebef25d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -34,9 +34,10 @@ class SwapDomainModule { userWalletManager: UserWalletManager, transactionManager: TransactionManager, @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - @SwapScope getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, @SwapScope sendTransactionUseCase: SendTransactionUseCase, quotesRepository: QuotesRepository, + swapTransactionRepository: SwapTransactionRepository, walletManagersFacade: WalletManagersFacade, coroutineDispatcherProvider: CoroutineDispatcherProvider, ): SwapInteractor { @@ -51,6 +52,7 @@ class SwapDomainModule { quotesRepository = quotesRepository, walletManagersFacade = walletManagersFacade, dispatcher = coroutineDispatcherProvider, + swapTransactionRepository = swapTransactionRepository, ) } @@ -69,7 +71,6 @@ class SwapDomainModule { return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder) } - @SwapScope @Provides @Singleton fun providesGetCryptoCurrencyStatusUseCase( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt index 6b59b50197..f2e6a4d139 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt @@ -1,5 +1,7 @@ package com.tangem.feature.swap.domain.models.domain +import java.math.BigDecimal + data class SavedSwapTransactionListModel( val userWalletId: String, val fromCryptoCurrencyId: String, @@ -10,10 +12,8 @@ data class SavedSwapTransactionListModel( data class SavedSwapTransactionModel( val txId: String, val timestamp: Long, - val fromCryptoAmount: String, - val toCryptoAmount: String, + val fromCryptoAmount: BigDecimal, + val toCryptoAmount: BigDecimal, val provider: SwapProvider, - val toFiatAmount: String? = null, - val fromFiatAmount: String? = null, val status: ExchangeStatusModel? = null, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt index 814723462c..09d8728073 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt @@ -8,6 +8,7 @@ data class SwapSuccessStateHolder( val txUrl: String, val fee: TextReference, val rate: TextReference, + val showStatusButton: Boolean, val providerName: TextReference, val providerType: TextReference, val providerIcon: String, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 0b18d866ad..b81bbb59e4 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType import com.tangem.feature.swap.domain.models.domain.NetworkInfo import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation @@ -592,6 +593,7 @@ internal class StateBuilder( txUrl = txUrl, providerName = TextReference.Str(providerState.name), providerType = TextReference.Str(providerState.type), + showStatusButton = providerState.type == ExchangeProviderType.CEX.name, providerIcon = providerState.iconUrl, fee = TextReference.Str("${fee.amountCrypto} ${fee.symbolCrypto} (${fee.amountFiatFormatted})"), rate = TextReference.Str(providerState.rate), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index ffb9334830..4c4221d250 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -43,6 +43,7 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) { SwapSuccessScreenButtons( textRes = R.string.common_close, txUrl = state.txUrl, + showStatusButton = state.showStatusButton, onExploreClick = state.onSecondaryButtonClick, onDoneClick = onBack, ) @@ -105,6 +106,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad private fun SwapSuccessScreenButtons( @StringRes textRes: Int, txUrl: String, + showStatusButton: Boolean, onExploreClick: () -> Unit, onDoneClick: () -> Unit, ) { @@ -119,21 +121,23 @@ private fun SwapSuccessScreenButtons( if (txUrl.isNotBlank()) { Row { SecondaryButtonIconStart( - text = stringResource(id = com.tangem.core.ui.R.string.common_explore), - iconResId = com.tangem.core.ui.R.drawable.ic_web_24, + text = stringResource(id = R.string.common_explore), + iconResId = R.drawable.ic_web_24, onClick = onExploreClick, modifier = Modifier.weight(1f), ) - SpacerW12() - SecondaryButtonIconStart( - text = stringResource(id = com.tangem.core.ui.R.string.common_share), - iconResId = com.tangem.core.ui.R.drawable.ic_share_24, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - context.shareText(txUrl) - }, - modifier = Modifier.weight(1f), - ) + if (showStatusButton) { + SpacerW12() + SecondaryButtonIconStart( + text = stringResource(id = R.string.express_cex_status_button_title), + iconResId = R.drawable.ic_arrow_top_right_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + context.shareText(txUrl) + }, + modifier = Modifier.weight(1f), + ) + } } SpacerH12() } @@ -154,6 +158,7 @@ private val state = SwapSuccessStateHolder( fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), providerName = TextReference.Str("1inch"), providerType = TextReference.Str(ExchangeProviderType.DEX.name), + showStatusButton = false, providerIcon = "", fromTokenAmount = TextReference.Str("1 000 DAI"), toTokenAmount = TextReference.Str("1 000 MATIC"), diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index c19e3f3577..d7592edba4 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation(deps.tangem.blockchain) implementation(deps.tangem.card.core) implementation(deps.timber) + implementation(deps.lifecycle.compose) /** DI */ implementation(deps.hilt.android) @@ -66,6 +67,9 @@ dependencies { implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + /** Temp dependency to swap domain */ + implementation(projects.features.swap.domain) + /** Feature Apis */ implementation(projects.features.tokendetails.api) implementation(projects.features.send.api) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index 9941c1bbda..353bc30d6d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -1,24 +1,28 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications -import kotlinx.collections.immutable.PersistentList +import kotlinx.coroutines.flow.MutableStateFlow internal data class SwapTransactionsState( val txId: String, val provider: SwapProvider, val txUrl: String? = null, - val timestamp: Long, - val statuses: PersistentList, - val activeStatus: ExchangeStatus?, + val timestamp: TextReference, val fiatSymbol: String, - val notification: ExchangeStatusNotifications? = null, + val activeStatus: MutableStateFlow, + val statuses: MutableStateFlow>, + val notification: MutableStateFlow = MutableStateFlow(null), + val toCryptoCurrencyId: CryptoCurrency.ID, val toCryptoAmount: String, val toCryptoSymbol: String, val toFiatAmount: String, val toCurrencyIcon: TokenIconState, + val fromCryptoCurrencyId: CryptoCurrency.ID, val fromCryptoAmount: String, val fromCryptoSymbol: String, val fromFiatAmount: String, @@ -29,6 +33,7 @@ internal data class SwapTransactionsState( internal class ExchangeStatusState( val status: ExchangeStatus, + val text: TextReference, val isActive: Boolean, val isDone: Boolean, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 1494ed003f..368164ce46 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -27,6 +27,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -242,6 +243,19 @@ internal class TokenDetailsStateFactory( return state.copy(notifications = notificationConverter.removeRentInfo(state)) } + fun getStateWithExchangeStatusBottomSheet(txId: String): TokenDetailsState { + val state = currentStateProvider() + return state.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissBottomSheet, + content = ExchangeStatusBottomSheetConfig( + value = state.swapTxs.first { it.txId == txId }, + ), + ), + ) + } + fun getStateAndTriggerEvent( state: TokenDetailsState, errorMessage: TextReference, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt new file mode 100644 index 0000000000..c0be53eb94 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -0,0 +1,227 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.toDateFormat +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel +import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import java.math.BigDecimal + +internal class TokenDetailsSwapTransactionsStateConverter( + private val clickIntents: TokenDetailsClickIntents, + appCurrencyProvider: Provider, +) : Converter> { + + private val iconStateConverter = CryptoCurrencyToIconStateConverter() + private val appCurrency = appCurrencyProvider() + + override fun convert(value: Unit): PersistentList { + return persistentListOf() + } + + fun convert( + savedTransactions: List, + cryptoStatusList: List, + ): PersistentList { + val result = mutableListOf() + + savedTransactions.forEach { swapCurrency -> + val firstStatus = cryptoStatusList.first { it.currency.id.value == swapCurrency.fromCryptoCurrencyId } + val secondStatus = cryptoStatusList.first { it.currency.id.value == swapCurrency.toCryptoCurrencyId } + + val (fromCurrency, toCurrency) = if (swapCurrency.fromCryptoCurrencyId == firstStatus.currency.id.value) { + firstStatus to secondStatus + } else { + secondStatus to firstStatus + } + + swapCurrency.transactions.forEach { transaction -> + val toAmount = transaction.toCryptoAmount + val fromAmount = transaction.fromCryptoAmount + val toFiatAmount = toAmount.multiply(toCurrency.value.fiatRate) + val fromFiatAmount = fromAmount.multiply(fromCurrency.value.fiatRate) + val timestamp = transaction.timestamp + result.add( + SwapTransactionsState( + txId = transaction.txId, + provider = transaction.provider, + txUrl = transaction.status?.txUrl, + timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"), + fiatSymbol = appCurrency.symbol, + statuses = MutableStateFlow(getStatuses(transaction.status?.status)), + activeStatus = MutableStateFlow(transaction.status?.status), + notification = MutableStateFlow( + getNotification( + transaction.status?.status, + transaction.status?.txUrl, + ), + ), + toCryptoCurrencyId = toCurrency.currency.id, + toCryptoAmount = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = toAmount, + cryptoCurrency = toCurrency.currency, + ), + toCryptoSymbol = toCurrency.currency.symbol, + toFiatAmount = getFiatAmount(toFiatAmount), + toCurrencyIcon = iconStateConverter.convert(toCurrency), + fromCryptoCurrencyId = fromCurrency.currency.id, + fromCryptoAmount = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = fromAmount, + cryptoCurrency = fromCurrency.currency, + ), + fromCryptoSymbol = fromCurrency.currency.symbol, + fromFiatAmount = getFiatAmount(fromFiatAmount), + fromCurrencyIcon = iconStateConverter.convert(fromCurrency), + onClick = { clickIntents.onSwapTransactionClick(transaction.txId) }, + onGoToProviderClick = { clickIntents.onGoToProviderClick(transaction.status?.txUrl.orEmpty()) }, + ), + ) + } + } + return result.toPersistentList() + } + + fun updateTxStatus(tx: SwapTransactionsState, status: ExchangeStatus?) { + if (tx.activeStatus.value == status) return + tx.activeStatus.update { status } + tx.notification.update { getNotification(status, tx.txUrl) } + tx.statuses.update { getStatuses(status) } + } + + private fun getFiatAmount(toFiatAmount: BigDecimal): String { + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = toFiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun getNotification(status: ExchangeStatus?, txUrl: String?): ExchangeStatusNotifications? { + if (txUrl == null) return null + return when (status) { + ExchangeStatus.Failed -> ExchangeStatusNotifications.Failed { + clickIntents.onGoToProviderClick(txUrl) + } + ExchangeStatus.Verifying -> ExchangeStatusNotifications.NeedVerification { + clickIntents.onGoToProviderClick(txUrl) + } + else -> null + } + } + + private fun getStatuses(status: ExchangeStatus?): List { + val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting + val isConfirming = status == ExchangeStatus.Confirming + val isVerifying = status == ExchangeStatus.Verifying + val isExchanging = status == ExchangeStatus.Exchanging + val isFailed = status == ExchangeStatus.Failed + val isSending = status == ExchangeStatus.Sending + + val isWaitingDone = !isWaiting + val isConfirmingDone = !isConfirming && isWaitingDone + val isExchangingDone = !isExchanging && isConfirmingDone + val isSendingDone = !isSending && !isVerifying && !isFailed && isExchangingDone + + return listOf( + waitStep(isWaiting, isWaitingDone), + confirmStep(isConfirming, isConfirmingDone), + exchangeStep(isExchanging, isExchangingDone, isVerifying, isFailed), + sendStep(isSending, isSendingDone), + ) + } + + private fun waitStep(isNew: Boolean, isNewDone: Boolean) = ExchangeStatusState( + status = ExchangeStatus.New, + text = when { + isNew -> combinedReference( + TextReference.Res(R.string.express_exchange_status_receiving), + TextReference.Str(STATUS_ACTIVE_DOTS), + ) + else -> TextReference.Res(R.string.express_exchange_status_receiving) + }, + isActive = isNew, + isDone = isNewDone, + ) + + private fun confirmStep(isConfirming: Boolean, isConfirmingDone: Boolean) = ExchangeStatusState( + status = ExchangeStatus.Confirming, + text = when { + isConfirming -> combinedReference( + TextReference.Res(R.string.express_exchange_status_confirming), + TextReference.Str(STATUS_ACTIVE_DOTS), + ) + isConfirmingDone -> TextReference.Res(R.string.express_exchange_status_confirmed) + else -> TextReference.Res(R.string.express_exchange_status_confirming) + }, + isActive = isConfirming, + isDone = isConfirmingDone, + ) + + private fun exchangeStep( + isExchanging: Boolean, + isExchangingDone: Boolean, + isVerifying: Boolean = false, + isFailed: Boolean = false, + ) = when { + isVerifying -> ExchangeStatusState( + status = ExchangeStatus.Verifying, + text = TextReference.Res(R.string.express_exchange_status_verifying), + isActive = true, + isDone = false, + ) + isFailed -> ExchangeStatusState( + status = ExchangeStatus.Failed, + text = TextReference.Res(R.string.express_exchange_status_failed), + isActive = true, + isDone = false, + ) + else -> ExchangeStatusState( + status = ExchangeStatus.Exchanging, + text = when { + isExchanging -> combinedReference( + TextReference.Res(R.string.express_exchange_status_exchanging), + TextReference.Str(STATUS_ACTIVE_DOTS), + ) + isExchangingDone -> TextReference.Res(R.string.express_exchange_status_exchanged) + else -> TextReference.Res(R.string.express_exchange_status_exchanging) + }, + isActive = isExchanging, + isDone = isExchangingDone, + ) + } + + private fun sendStep(isSending: Boolean, isSendingDone: Boolean) = ExchangeStatusState( + status = ExchangeStatus.Sending, + text = when { + isSending -> combinedReference( + TextReference.Res(R.string.express_exchange_status_sending), + TextReference.Str(STATUS_ACTIVE_DOTS), + ) + isSendingDone -> TextReference.Res(R.string.express_exchange_status_sent) + else -> TextReference.Res(R.string.express_exchange_status_sending) + }, + isActive = isSending, + isDone = isSendingDone, + ) + + private companion object { + private const val STATUS_ACTIVE_DOTS = "…" + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt index c33b4fc290..6aee05217a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt @@ -22,7 +22,9 @@ internal fun ExchangeEstimate( fromTokenIconState: TokenIconState, toTokenIconState: TokenIconState, fromCryptoAmount: TextReference, + fromCryptoSymbol: String, toCryptoAmount: TextReference, + toCryptoSymbol: String, fromFiatAmount: TextReference, toFiatAmount: TextReference, modifier: Modifier = Modifier, @@ -59,9 +61,11 @@ internal fun ExchangeEstimate( leftIcon = fromTokenIconState, leftTitle = fromCryptoAmount, leftSubtitle = fromFiatAmount, + leftTitleEllipsisOffset = fromCryptoSymbol.length, rightIcon = toTokenIconState, rightTitle = toCryptoAmount, rightSubtitle = toFiatAmount, + rightTitleEllipsisOffset = toCryptoSymbol.length, ) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index b9d7dd77ee..909849ae07 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -17,19 +17,22 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState import com.tangem.features.tokendetails.impl.R -import kotlinx.collections.immutable.PersistentList +import kotlinx.coroutines.flow.MutableStateFlow @Composable internal fun ExchangeStatusBlock( - statuses: PersistentList, + statuses: MutableStateFlow>, onClick: () -> Unit, modifier: Modifier = Modifier, ) { + val statusValues = statuses.collectAsStateWithLifecycle() Column( modifier = modifier .clip(TangemTheme.shapes.roundedCornersXMedium) @@ -70,10 +73,10 @@ internal fun ExchangeStatusBlock( } } - statuses.forEachIndexed { index, item -> + statusValues.value.forEachIndexed { index, item -> ExchangeStatusStep( stepStatus = item, - isLast = index == statuses.lastIndex, + isLast = index == statusValues.value.lastIndex, ) } } @@ -128,7 +131,7 @@ private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) { } Text( - text = getStatusText(stepStatus)?.let { stringResource(it) }.orEmpty(), + text = stepStatus.text.resolveReference(), style = TangemTheme.typography.body2, color = textColor, modifier = Modifier @@ -206,34 +209,4 @@ private fun ExchangeStepSeparator() { shape = CircleShape, ), ) -} - -private fun getStatusText(stepStatus: ExchangeStatusState) = when (stepStatus.status) { - ExchangeStatus.Failed -> R.string.express_exchange_status_failed - ExchangeStatus.Verifying -> if (stepStatus.isDone) { - R.string.express_exchange_status_verified - } else { - R.string.express_exchange_status_verifying - } - ExchangeStatus.New, ExchangeStatus.Waiting -> if (stepStatus.isDone) { - R.string.express_exchange_status_received - } else { - R.string.express_exchange_status_receiving - } - ExchangeStatus.Confirming -> if (stepStatus.isDone) { - R.string.express_exchange_status_confirmed - } else { - R.string.express_exchange_status_confirming - } - ExchangeStatus.Exchanging -> if (stepStatus.isDone) { - R.string.express_exchange_status_exchanged - } else { - R.string.express_exchange_status_exchanging - } - ExchangeStatus.Sending -> if (stepStatus.isDone) { - R.string.express_exchange_status_sent - } else { - R.string.express_exchange_status_sending - } - else -> null } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index b32c69f48e..f02ca3d0a7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH10 import com.tangem.core.ui.components.SpacerH12 @@ -19,8 +20,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.toDateFormat -import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState @Composable @@ -36,6 +36,8 @@ internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) { val config = content.value + val status = config.activeStatus.collectAsStateWithLifecycle() + val notification = config.notification.collectAsStateWithLifecycle() Column( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { @@ -55,13 +57,14 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC .align(CenterHorizontally), ) SpacerH16() - val timestamp = config.timestamp ExchangeEstimate( - timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"), + timestamp = config.timestamp, fromTokenIconState = config.fromCurrencyIcon, toTokenIconState = config.toCurrencyIcon, fromCryptoAmount = TextReference.Str(config.fromCryptoAmount), + fromCryptoSymbol = config.fromCryptoSymbol, toCryptoAmount = TextReference.Str(config.toCryptoAmount), + toCryptoSymbol = config.toCryptoSymbol, fromFiatAmount = TextReference.Str(config.fromFiatAmount), toFiatAmount = TextReference.Str(config.toFiatAmount), ) @@ -77,12 +80,20 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC onClick = config.onGoToProviderClick, ) AnimatedContent( - targetState = config.notification, + targetState = notification.value, label = "Exchange Status Notification Change", ) { it?.let { - SpacerH12() - Notification(config = it.config) + val tint = when (status.value) { + ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention + ExchangeStatus.Failed -> TangemTheme.colors.icon.warning + else -> null + } + Notification( + config = it.config, + iconTint = tint, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + ) } } SpacerH24() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index 4ea9a31b08..79c9ab8392 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -4,9 +4,7 @@ import androidx.annotation.DrawableRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -16,9 +14,11 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.constraintlayout.compose.ConstraintLayout -import androidx.constraintlayout.compose.Dimension -import androidx.constraintlayout.compose.Visibility +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.constraintlayout.compose.* +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.tokenicon.TokenIcon @@ -41,8 +41,8 @@ internal fun LazyListScope.swapTransactionsItems( contentType = { swapTxs[it]::class.java }, ) { val item = swapTxs[it] - - val (iconRes, tint) = when (item.activeStatus) { + val status = item.activeStatus.collectAsStateWithLifecycle() + val (iconRes, tint) = when (status.value) { ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention ExchangeStatus.Failed -> R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning else -> null to null @@ -54,7 +54,6 @@ internal fun LazyListScope.swapTransactionsItems( toTokenIconState = item.toCurrencyIcon, fromAmount = item.fromCryptoAmount, fromSymbol = item.fromCryptoSymbol, - toAmount = item.toCryptoAmount, toSymbol = item.toCryptoSymbol, onClick = item.onClick, infoIconRes = iconRes, @@ -73,7 +72,6 @@ private fun ExchangeStatusItem( toTokenIconState: TokenIconState, fromAmount: String, fromSymbol: String, - toAmount: String, toSymbol: String, onClick: () -> Unit, modifier: Modifier = Modifier, @@ -121,7 +119,7 @@ private fun ExchangeStatusItem( top.linkTo(titleRef.bottom, padding6) end.linkTo(swapIconRef.start) bottom.linkTo(parent.bottom) - width = Dimension.fillToConstraints + width = Dimension.fillToConstraints.atMostWrapContent }, ) Icon( @@ -149,17 +147,16 @@ private fun ExchangeStatusItem( bottom.linkTo(parent.bottom) }, ) - EllipsisText( - text = toAmount, + Text( + text = toSymbol, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, - ellipsis = TextEllipsis.OffsetEnd(toSymbol.length), modifier = Modifier.constrainAs(toRef) { start.linkTo(toIconRef.end, padding6) top.linkTo(titleRef.bottom, padding6) - end.linkTo(infoIconRef.start, padding6) + end.linkTo(infoIconRef.start, padding6, padding6) bottom.linkTo(parent.bottom) - width = Dimension.fillToConstraints + width = Dimension.fillToConstraints.atLeastWrapContent }, ) Icon( @@ -192,4 +189,54 @@ private fun ExchangeStatusItem( }, ) } -} \ No newline at end of file +} + +//region Preview +@Preview +@Composable +private fun ExchangeStatusItemPreview_Light( + @PreviewParameter(ExchangeStatusItemsPreviewParameterProvider::class) amount: String, +) { + TangemTheme { + ExchangeStatusItem( + providerName = "ChangeNow", + fromTokenIconState = TokenIconState.Loading, + toTokenIconState = TokenIconState.Loading, + fromAmount = amount, + fromSymbol = "USDT", + toSymbol = "USDT", + onClick = {}, + infoIconRes = null, + infoIconTint = null, + ) + } +} + +@Preview +@Composable +private fun ExchangeStatusItemPreview_Dark( + @PreviewParameter(ExchangeStatusItemsPreviewParameterProvider::class) amount: String, +) { + TangemTheme(isDark = true) { + ExchangeStatusItem( + providerName = "ChangeNow", + fromTokenIconState = TokenIconState.Loading, + toTokenIconState = TokenIconState.Loading, + fromAmount = amount, + fromSymbol = "USDT", + toSymbol = "USDT", + onClick = {}, + infoIconRes = null, + infoIconTint = null, + ) + } +} + +private class ExchangeStatusItemsPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + "1111111111111111111111111111 USDT", + "11111 USDT", + ) +} +//endregion \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt new file mode 100644 index 0000000000..d92306d03c --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -0,0 +1,141 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels + +import arrow.core.getOrElse +import com.tangem.common.Provider +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.SwapTransactionRepository +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel +import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel +import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext + +@Suppress("LongParameterList") +internal class ExchangeStatusFactory( + private val swapTransactionRepository: SwapTransactionRepository, + private val swapRepository: SwapRepository, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val dispatchers: CoroutineDispatcherProvider, + private val clickIntents: TokenDetailsClickIntents, + private val appCurrencyProvider: Provider, + private val userWalletId: UserWalletId, + private val cryptoCurrencyId: CryptoCurrency.ID, +) { + + private val swapTransactionsStateConverter by lazy { + TokenDetailsSwapTransactionsStateConverter( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + ) + } + + operator fun invoke() = combine( + flow = swapTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId), + flow2 = getWalletCryptoCurrencies().conflate(), + ) { savedTransactions, cryptoCurrenciesStatusList -> + innerLoadSwapState( + savedTransactions, + cryptoCurrenciesStatusList, + ) + } + + private fun getWalletCryptoCurrencies() = flow { + val selectedWallet = getSelectedWalletSyncUseCase().fold( + ifLeft = { null }, + ifRight = { it }, + ) + requireNotNull(selectedWallet) { "No selected wallet" } + + val cryptoCurrenciesList = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) + .getOrElse { emptyList() } + + emit(cryptoCurrenciesList) + } + + suspend fun updateSwapTxStatuses(swapTxList: PersistentList) = withContext(dispatchers.io) { + swapTxList.map { tx -> + async { + val status = getExchangeStatus(tx.txId)?.status + swapTransactionsStateConverter.updateTxStatus(tx, status) + tx.removeIfFinished(status) + } + } + .awaitAll() + .filterNotNull() + .toPersistentList() + } + + private suspend fun innerLoadSwapState( + savedTransactions: List?, + cryptoCurrenciesStatusList: List, + ): PersistentList { + val txWithStatuses = savedTransactions?.map { currencySwaps -> + currencySwaps.copy( + transactions = currencySwaps.transactions.map { tx -> + val status = getExchangeStatus(tx.txId) + tx.copy( + status = status, + ) + }, + ) + } + + return getExchangeStatusState( + savedTransactions = txWithStatuses, + cryptoCurrencyStatusList = cryptoCurrenciesStatusList, + ) + } + + private suspend fun getExchangeStatus(txId: String): ExchangeStatusModel? { + return swapRepository.getExchangeStatus(txId) + .fold( + ifLeft = { null }, + ifRight = { it }, + ) + } + + private fun getExchangeStatusState( + savedTransactions: List?, + cryptoCurrencyStatusList: List, + ): PersistentList { + if (savedTransactions == null || cryptoCurrencyStatusList.isEmpty()) { + return persistentListOf() + } + + return swapTransactionsStateConverter.convert( + savedTransactions = savedTransactions, + cryptoStatusList = cryptoCurrencyStatusList, + ) + } + + private suspend fun SwapTransactionsState.removeIfFinished(status: ExchangeStatus?): SwapTransactionsState? { + return if (status == ExchangeStatus.Refunded || status == ExchangeStatus.Finished) { + swapTransactionRepository.removeTransaction( + userWalletId = userWalletId, + fromCryptoCurrencyId = fromCryptoCurrencyId, + toCryptoCurrencyId = toCryptoCurrencyId, + txId = txId, + ) + null + } else { + this + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 992915899c..95093f7fdb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -38,4 +38,8 @@ interface TokenDetailsClickIntents { fun onDismissBottomSheet() fun onCloseRentInfoNotification() + + fun onSwapTransactionClick(txId: String) + + fun onGoToProviderClick(url: String) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 55cb6c48be..126298c471 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -31,16 +31,19 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter +import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.features.tokendetails.impl.R import com.tangem.features.tokendetails.navigation.TokenDetailsRouter -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.PersistentList import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* @@ -50,7 +53,7 @@ import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LongParameterList", "LargeClass") +@Suppress("LongParameterList", "LargeClass", "TooManyFunctions") @HiltViewModel internal class TokenDetailsViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, @@ -67,6 +70,10 @@ internal class TokenDetailsViewModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val swapRepository: SwapRepository, + private val swapTransactionRepository: SwapTransactionRepository, private val walletManagersFacade: WalletManagersFacade, private val isDemoCardUseCase: IsDemoCardUseCase, private val reduxStateHolder: ReduxStateHolder, @@ -86,8 +93,11 @@ internal class TokenDetailsViewModel @Inject constructor( private val marketPriceJobHolder = JobHolder() private val refreshStateJobHolder = JobHolder() private val warningsJobHolder = JobHolder() + private val swapTxJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null + private var swapTxStatusTaskScheduler = SingleTaskScheduler>() + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() private val stateFactory = TokenDetailsStateFactory( @@ -98,6 +108,20 @@ internal class TokenDetailsViewModel @Inject constructor( decimals = cryptoCurrency.decimals, ) + private val exchangeStatusFactory by lazy { + ExchangeStatusFactory( + swapTransactionRepository = swapTransactionRepository, + swapRepository = swapRepository, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, + dispatchers = dispatchers, + clickIntents = this, + appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrency.id, + ) + } + var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency)) private set @@ -109,8 +133,14 @@ internal class TokenDetailsViewModel @Inject constructor( handleBalanceHiding(owner) } + override fun onCleared() { + swapTxStatusTaskScheduler.cancelTask() + super.onCleared() + } + private fun updateContent() { subscribeOnCurrencyStatusUpdates() + subscribeOnExchangeTransactionsUpdates() updateTxHistory(refresh = false, showItemsLoading = true) } @@ -177,6 +207,33 @@ internal class TokenDetailsViewModel @Inject constructor( } } + private fun subscribeOnExchangeTransactionsUpdates() { + viewModelScope.launch(dispatchers.io) { + swapTxStatusTaskScheduler.cancelTask() + exchangeStatusFactory.invoke() + .onEach { swapTxs -> + swapTxStatusTaskScheduler.scheduleTask( + viewModelScope, + PeriodicTask( + delay = EXCHANGE_STATUS_UPDATE_DELAY, + task = { + runCatching(dispatchers.io) { + exchangeStatusFactory.updateSwapTxStatuses(swapTxs) + } + }, + onSuccess = { updatedTxs -> + uiState = uiState.copy(swapTxs = updatedTxs) + }, + onError = {}, + ), + ) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(swapTxJobHolder) + } + } + /** * @param refresh - invalidate cache and get data from remote * @param showItemsLoading - show loading items placeholder. @@ -475,35 +532,16 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onCloseRentInfoNotification() { uiState = stateFactory.getStateWithRemovedRentNotification() } -} -// -// val text = when (stepStatus.status) { -// ExchangeStatus.Failed -> stringResource(id = R.string.express_exchange_status_failed) -// ExchangeStatus.Verifying -> if (stepStatus.isDone) { -// stringResource(id = R.string.express_exchange_status_verified) -// } else { -// stringResource(id = R.string.express_exchange_status_verifying) -// } -// ExchangeStatus.New, ExchangeStatus.Waiting -> if (stepStatus.isDone) { -// stringResource(id = R.string.express_exchange_status_received) -// } else { -// stringResource(id = R.string.express_exchange_status_receiving) -// } -// ExchangeStatus.Confirming -> if (stepStatus.isDone) { -// stringResource(id = R.string.express_exchange_status_confirmed) -// } else { -// stringResource(id = R.string.express_exchange_status_confirming) -// } -// ExchangeStatus.Exchanging -> if (stepStatus.isDone) { -// stringResource(id = R.string.express_exchange_status_exchanged) -// } else { -// stringResource(id = R.string.express_exchange_status_exchanging) -// } -// ExchangeStatus.Sending -> if (stepStatus.isDone) { -// stringResource(id = R.string.express_exchange_status_sent) -// } else { -// stringResource(id = R.string.express_exchange_status_sending) -// } -// else -> "" -// } \ No newline at end of file + override fun onSwapTransactionClick(txId: String) { + uiState = stateFactory.getStateWithExchangeStatusBottomSheet(txId) + } + + override fun onGoToProviderClick(url: String) { + router.openUrl(url) + } + + private companion object { + const val EXCHANGE_STATUS_UPDATE_DELAY = 10_000L + } +} \ No newline at end of file From be56fd039a7f78faade5de6e9527e68300b68a74 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 17:51:22 +0300 Subject: [PATCH 093/139] Updated on 2026-08-14 --- .../tangem/datasource/api/express/TangemExpressApi.kt | 2 ++ .../java/com/tangem/feature/swap/SwapRepositoryImpl.kt | 4 ++++ .../tangem/feature/swap/domain/SwapInteractorImpl.kt | 10 +++++++--- .../com/tangem/feature/swap/domain/SwapRepository.kt | 2 ++ .../com/tangem/feature/swap/ui/SwapScreenContent.kt | 6 ++++++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index b5ef12459c..b78d485fa6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -32,6 +32,7 @@ interface TangemExpressApi { @Query("toNetwork") toNetwork: String, @Query("fromAmount") fromAmount: String, @Query("fromDecimals") fromDecimals: Int, + @Query("toDecimals") toDecimals: Int, @Query("providerId") providerId: String, @Query("rateType") rateType: String, ): ApiResponse @@ -44,6 +45,7 @@ interface TangemExpressApi { @Query("toNetwork") toNetwork: String, @Query("fromAmount") fromAmount: String, @Query("fromDecimals") fromDecimals: Int, + @Query("toDecimals") toDecimals: Int, @Query("providerId") providerId: String, @Query("rateType") rateType: String, @Query("toAddress") toAddress: String, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 338fafe196..90358d75df 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -168,6 +168,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork: String, fromAmount: String, fromDecimals: Int, + toDecimals: Int, providerId: String, rateType: RateType, ): AggregatedSwapDataModel { @@ -180,6 +181,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork = toNetwork, fromAmount = fromAmount, fromDecimals = fromDecimals, + toDecimals = toDecimals, providerId = providerId, rateType = rateType.name.lowercase(), ).getOrThrow() @@ -208,6 +210,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork: String, fromAmount: String, fromDecimals: Int, + toDecimals: Int, providerId: String, rateType: RateType, toAddress: String, @@ -221,6 +224,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork = toNetwork, fromAmount = fromAmount, fromDecimals = fromDecimals, + toDecimals = toDecimals, providerId = providerId, rateType = rateType.name.lowercase(), toAddress = toAddress, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index b4e0208243..685e925ce8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -270,6 +270,7 @@ internal class SwapInteractorImpl @Inject constructor( toNetwork = toToken.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, + toDecimals = toToken.currency.decimals, providerId = provider.providerId, rateType = RateType.FLOAT, ) @@ -462,6 +463,7 @@ internal class SwapInteractorImpl @Inject constructor( toNetwork = currencyToGet.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, + toDecimals = currencyToGet.currency.decimals, providerId = swapProvider.providerId, rateType = RateType.FLOAT, toAddress = currencyToGet.value.networkAddress?.defaultAddress ?: "", @@ -652,6 +654,7 @@ internal class SwapInteractorImpl @Inject constructor( fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, providerId = provider.providerId, + toDecimals = toToken.decimals, rateType = RateType.FLOAT, ) @@ -707,7 +710,7 @@ internal class SwapInteractorImpl @Inject constructor( swapAmount = amount, quotesLoadedState = swapState, isAllowedToSpend = isAllowedToSpend, - spenderAddress = requireNotNull(quoteModel.allowanceContract) { "Allowance contract is null" }, + spenderAddress = quoteModel.allowanceContract, ) state.copy( preparedSwapConfigState = state.preparedSwapConfigState.copy( @@ -776,6 +779,7 @@ internal class SwapInteractorImpl @Inject constructor( toNetwork = toToken.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, + toDecimals = toToken.currency.decimals, providerId = provider.providerId, rateType = RateType.FLOAT, toAddress = toToken.value.networkAddress?.defaultAddress ?: "", @@ -907,7 +911,7 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenStatus: CryptoCurrencyStatus, swapAmount: SwapAmount, quotesLoadedState: SwapState.QuotesLoadedState, - spenderAddress: String, + spenderAddress: String?, isAllowedToSpend: Boolean, ): SwapState.QuotesLoadedState { val fromToken = fromTokenStatus.currency @@ -934,7 +938,7 @@ internal class SwapInteractorImpl @Inject constructor( derivationPath = derivationPath, fromToken = fromToken, swapAmount = swapAmount, - spenderAddress = spenderAddress, + spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" }, ) val feeData = try { transactionManager.getFee( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index c96bb13b19..9f90d4dc3d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -25,6 +25,7 @@ interface SwapRepository { toNetwork: String, fromAmount: String, fromDecimals: Int, + toDecimals: Int, providerId: String, rateType: RateType, ): AggregatedSwapDataModel @@ -72,6 +73,7 @@ interface SwapRepository { toNetwork: String, fromAmount: String, fromDecimals: Int, + toDecimals: Int, providerId: String, rateType: RateType, toAddress: String, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 95fd4ce997..cd6c2ea2fa 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -294,6 +294,12 @@ private fun SwapWarnings(warnings: List) { config = warning.notificationConfig, ) } + is SwapWarning.GeneralWarning -> { + Notification( + config = warning.notificationConfig, + iconTint = TangemTheme.colors.icon.warning, + ) + } else -> {} } SpacerH8() From 59dff6b9bd52086f9e83928b6dbe7c2801482b92 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 18:11:28 +0300 Subject: [PATCH 094/139] Updated on 2026-08-14 --- .../tangem/datasource/api/express/TangemExpressApi.kt | 2 ++ .../java/com/tangem/feature/swap/SwapRepositoryImpl.kt | 4 ++++ .../tangem/feature/swap/domain/SwapInteractorImpl.kt | 10 +++++++--- .../com/tangem/feature/swap/domain/SwapRepository.kt | 2 ++ .../com/tangem/feature/swap/ui/SwapScreenContent.kt | 6 ++++++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index b5ef12459c..b78d485fa6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -32,6 +32,7 @@ interface TangemExpressApi { @Query("toNetwork") toNetwork: String, @Query("fromAmount") fromAmount: String, @Query("fromDecimals") fromDecimals: Int, + @Query("toDecimals") toDecimals: Int, @Query("providerId") providerId: String, @Query("rateType") rateType: String, ): ApiResponse @@ -44,6 +45,7 @@ interface TangemExpressApi { @Query("toNetwork") toNetwork: String, @Query("fromAmount") fromAmount: String, @Query("fromDecimals") fromDecimals: Int, + @Query("toDecimals") toDecimals: Int, @Query("providerId") providerId: String, @Query("rateType") rateType: String, @Query("toAddress") toAddress: String, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 338fafe196..90358d75df 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -168,6 +168,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork: String, fromAmount: String, fromDecimals: Int, + toDecimals: Int, providerId: String, rateType: RateType, ): AggregatedSwapDataModel { @@ -180,6 +181,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork = toNetwork, fromAmount = fromAmount, fromDecimals = fromDecimals, + toDecimals = toDecimals, providerId = providerId, rateType = rateType.name.lowercase(), ).getOrThrow() @@ -208,6 +210,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork: String, fromAmount: String, fromDecimals: Int, + toDecimals: Int, providerId: String, rateType: RateType, toAddress: String, @@ -221,6 +224,7 @@ internal class SwapRepositoryImpl @Inject constructor( toNetwork = toNetwork, fromAmount = fromAmount, fromDecimals = fromDecimals, + toDecimals = toDecimals, providerId = providerId, rateType = rateType.name.lowercase(), toAddress = toAddress, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index b4e0208243..685e925ce8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -270,6 +270,7 @@ internal class SwapInteractorImpl @Inject constructor( toNetwork = toToken.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, + toDecimals = toToken.currency.decimals, providerId = provider.providerId, rateType = RateType.FLOAT, ) @@ -462,6 +463,7 @@ internal class SwapInteractorImpl @Inject constructor( toNetwork = currencyToGet.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, + toDecimals = currencyToGet.currency.decimals, providerId = swapProvider.providerId, rateType = RateType.FLOAT, toAddress = currencyToGet.value.networkAddress?.defaultAddress ?: "", @@ -652,6 +654,7 @@ internal class SwapInteractorImpl @Inject constructor( fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, providerId = provider.providerId, + toDecimals = toToken.decimals, rateType = RateType.FLOAT, ) @@ -707,7 +710,7 @@ internal class SwapInteractorImpl @Inject constructor( swapAmount = amount, quotesLoadedState = swapState, isAllowedToSpend = isAllowedToSpend, - spenderAddress = requireNotNull(quoteModel.allowanceContract) { "Allowance contract is null" }, + spenderAddress = quoteModel.allowanceContract, ) state.copy( preparedSwapConfigState = state.preparedSwapConfigState.copy( @@ -776,6 +779,7 @@ internal class SwapInteractorImpl @Inject constructor( toNetwork = toToken.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, + toDecimals = toToken.currency.decimals, providerId = provider.providerId, rateType = RateType.FLOAT, toAddress = toToken.value.networkAddress?.defaultAddress ?: "", @@ -907,7 +911,7 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenStatus: CryptoCurrencyStatus, swapAmount: SwapAmount, quotesLoadedState: SwapState.QuotesLoadedState, - spenderAddress: String, + spenderAddress: String?, isAllowedToSpend: Boolean, ): SwapState.QuotesLoadedState { val fromToken = fromTokenStatus.currency @@ -934,7 +938,7 @@ internal class SwapInteractorImpl @Inject constructor( derivationPath = derivationPath, fromToken = fromToken, swapAmount = swapAmount, - spenderAddress = spenderAddress, + spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" }, ) val feeData = try { transactionManager.getFee( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index c96bb13b19..9f90d4dc3d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -25,6 +25,7 @@ interface SwapRepository { toNetwork: String, fromAmount: String, fromDecimals: Int, + toDecimals: Int, providerId: String, rateType: RateType, ): AggregatedSwapDataModel @@ -72,6 +73,7 @@ interface SwapRepository { toNetwork: String, fromAmount: String, fromDecimals: Int, + toDecimals: Int, providerId: String, rateType: RateType, toAddress: String, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 95fd4ce997..cd6c2ea2fa 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -294,6 +294,12 @@ private fun SwapWarnings(warnings: List) { config = warning.notificationConfig, ) } + is SwapWarning.GeneralWarning -> { + Notification( + config = warning.notificationConfig, + iconTint = TangemTheme.colors.icon.warning, + ) + } else -> {} } SpacerH8() From 61fbf9a6ea4a04ee6d8f0ece83504014bd4ae71b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 18:14:51 +0300 Subject: [PATCH 095/139] Updated on 2026-08-14 --- .../feature/swap/models/SwapStateHolder.kt | 3 +- .../tangem/feature/swap/ui/StateBuilder.kt | 11 +- .../feature/swap/ui/SwapScreenContent.kt | 13 +- .../tangem/feature/swap/ui/TransactionCard.kt | 175 +++++++++--------- 4 files changed, 103 insertions(+), 99 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 9fc0d8a9c1..e30eff225c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.models +import androidx.annotation.DrawableRes import androidx.compose.ui.text.input.TextFieldValue import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig @@ -14,7 +15,6 @@ data class SwapStateHolder( val sendCardData: SwapCardState, val receiveCardData: SwapCardState, val networkCurrency: String, - val networkId: String, val blockchainId: String, // not the same as networkId, its local id in app val warnings: List = emptyList(), val alert: SwapWarning.GenericWarning? = null, @@ -42,6 +42,7 @@ data class SwapStateHolder( sealed class SwapCardState { data class SwapCardData( + @DrawableRes val networkIconRes: Int?, val type: TransactionCardType, val amountEquivalent: String?, val token: CryptoCurrencyStatus?, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index b81bbb59e4..4bab76b4a3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -49,7 +49,6 @@ internal class StateBuilder( fun createInitialLoadingState(initialCurrency: CryptoCurrency, networkInfo: NetworkInfo): SwapStateHolder { return SwapStateHolder( - networkId = initialCurrency.network.backendId, blockchainId = networkInfo.blockchainId, sendCardData = SwapCardState.SwapCardData( type = TransactionCardType.SendCard(actions.onAmountChanged, actions.onAmountSelected), @@ -62,6 +61,7 @@ internal class StateBuilder( canSelectAnotherToken = false, isNotNativeToken = initialCurrency is CryptoCurrency.Token, balance = "", + networkIconRes = getActiveIconRes(initialCurrency.network.id.value), isBalanceHidden = true, ), receiveCardData = SwapCardState.SwapCardData( @@ -74,6 +74,7 @@ internal class StateBuilder( canSelectAnotherToken = false, balance = "", isNotNativeToken = false, + networkIconRes = null, coinId = null, isBalanceHidden = true, ), @@ -109,6 +110,7 @@ internal class StateBuilder( tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = fromToken.getFormattedAmount(), + networkIconRes = getActiveIconRes(fromToken.currency.network.id.value), isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardState.Empty( @@ -163,6 +165,7 @@ internal class StateBuilder( isNotNativeToken = fromToken is CryptoCurrency.Token, canSelectAnotherToken = canSelectSendToken, balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "", + networkIconRes = getActiveIconRes(fromToken.network.id.value), isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardState.SwapCardData( @@ -176,6 +179,7 @@ internal class StateBuilder( isNotNativeToken = toToken is CryptoCurrency.Token, canSelectAnotherToken = canSelectReceiveToken, balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "", + networkIconRes = getActiveIconRes(toToken.network.id.value), isBalanceHidden = isBalanceHiddenProvider(), ), warnings = emptyList(), @@ -256,6 +260,7 @@ internal class StateBuilder( isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, + networkIconRes = uiStateHolder.sendCardData.networkIconRes, balance = fromCurrencyStatus.getFormattedAmount(), isBalanceHidden = isBalanceHiddenProvider(), ), @@ -269,6 +274,7 @@ internal class StateBuilder( isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, + networkIconRes = uiStateHolder.receiveCardData.networkIconRes, balance = toCurrencyStatus.getFormattedAmount(), isBalanceHidden = isBalanceHiddenProvider(), ), @@ -329,6 +335,7 @@ internal class StateBuilder( isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, + networkIconRes = uiStateHolder.receiveCardData.networkIconRes, balance = toToken.getFormattedAmount(), isBalanceHidden = isBalanceHiddenProvider(), ) @@ -421,6 +428,7 @@ internal class StateBuilder( isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, + networkIconRes = uiStateHolder.sendCardData.networkIconRes, balance = emptyAmountState.fromTokenWalletBalance, isBalanceHidden = isBalanceHiddenProvider(), ), @@ -434,6 +442,7 @@ internal class StateBuilder( isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, + networkIconRes = uiStateHolder.receiveCardData.networkIconRes, balance = emptyAmountState.toTokenWalletBalance, isBalanceHidden = isBalanceHiddenProvider(), ), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index cd6c2ea2fa..0ba2059fab 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -21,7 +20,6 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.extensions.getActiveIconResByCoinId import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme @@ -135,14 +133,10 @@ private fun MainInfo(state: SwapStateHolder) { ConstraintLayout( modifier = Modifier.fillMaxWidth(), ) { - val networkIconRes = remember { - getActiveIconRes(state.blockchainId) - } val (topCard, bottomCard, button) = createRefs() val priceImpactWarning = state.warnings.filterIsInstance().firstOrNull() TransactionCardData( priceImpactWarning = priceImpactWarning, - networkIconRes = networkIconRes, swapCardState = state.sendCardData, modifier = Modifier.constrainAs(topCard) { top.linkTo(parent.top) @@ -152,7 +146,6 @@ private fun MainInfo(state: SwapStateHolder) { val marginCard = TangemTheme.dimens.spacing16 TransactionCardData( priceImpactWarning = priceImpactWarning, - networkIconRes = networkIconRes, swapCardState = state.receiveCardData, modifier = Modifier.constrainAs(bottomCard) { top.linkTo(topCard.bottom, margin = marginCard) @@ -174,7 +167,6 @@ private fun MainInfo(state: SwapStateHolder) { @Composable private fun TransactionCardData( priceImpactWarning: SwapWarning.HighPriceImpact?, - networkIconRes: Int?, swapCardState: SwapCardState, onSelectTokenClick: (() -> Unit)?, modifier: Modifier = Modifier, @@ -202,7 +194,7 @@ private fun TransactionCardData( tokenIconUrl = swapCardState.tokenIconUrl ?: "", tokenCurrency = swapCardState.tokenCurrency, priceImpact = priceImpactWarning, - networkIconRes = if (swapCardState.isNotNativeToken) networkIconRes else null, + networkIconRes = if (swapCardState.isNotNativeToken) swapCardState.networkIconRes else null, iconPlaceholder = swapCardState.coinId?.let { getActiveIconResByCoinId(it) }, @@ -355,6 +347,7 @@ private val sendCard = SwapCardState.SwapCardData( balance = "123", coinId = "", token = null, + networkIconRes = R.drawable.img_polygon_22, isBalanceHidden = false, ) @@ -369,11 +362,11 @@ private val receiveCard = SwapCardState.SwapCardData( balance = "33333", coinId = "", token = null, + networkIconRes = R.drawable.img_polygon_22, isBalanceHidden = false, ) private val state = SwapStateHolder( - networkId = "ethereum", sendCardData = sendCard, receiveCardData = receiveCard, fee = FeeItemState.Content( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index d5996c36a5..f4bd0ae799 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -17,6 +16,7 @@ import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext @@ -55,53 +55,53 @@ fun TransactionCard( @DrawableRes networkIconRes: Int? = null, onChangeTokenClick: (() -> Unit)? = null, ) { - Card( - shape = RoundedCornerShape(TangemTheme.dimens.radius12), - backgroundColor = TangemTheme.colors.background.primary, - elevation = TangemTheme.dimens.elevation2, - modifier = modifier, + Box( + modifier = modifier + .background( + shape = RoundedCornerShape(TangemTheme.dimens.radius12), + color = TangemTheme.colors.background.primary, + ) + .fillMaxSize(), ) { - Box(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxWidth(), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.Start, - ) { - Header(balance = stringResource(R.string.common_balance, balance), type = type) + Column( + modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start, + ) { + Header(balance = stringResource(R.string.common_balance, balance), type = type) - Content( - type = type, - amountEquivalent = amountEquivalent, - textFieldValue = textFieldValue, - priceImpact = priceImpact, - ) - } + Content( + type = type, + amountEquivalent = amountEquivalent, + textFieldValue = textFieldValue, + priceImpact = priceImpact, + ) + } - Box(modifier = Modifier.align(Alignment.BottomEnd)) { - Token( - tokenIconUrl = tokenIconUrl, - tokenCurrency = tokenCurrency, - networkIconRes = networkIconRes, - iconPlaceholder = iconPlaceholder, - ) - } + Box(modifier = Modifier.align(Alignment.BottomEnd)) { + Token( + tokenIconUrl = tokenIconUrl, + tokenCurrency = tokenCurrency, + networkIconRes = networkIconRes, + iconPlaceholder = iconPlaceholder, + ) + } - if (onChangeTokenClick != null) { - Box(modifier = Modifier.align(Alignment.CenterEnd)) { - ChangeTokenSelector() - } - Box( - Modifier - .align(Alignment.CenterEnd) - .height(TangemTheme.dimens.size116) - .width(TangemTheme.dimens.size102) - .clickable( - indication = rememberRipple(bounded = false), - interactionSource = remember { MutableInteractionSource() }, - ) { onChangeTokenClick() }, - ) + if (onChangeTokenClick != null) { + Box(modifier = Modifier.align(Alignment.CenterEnd)) { + ChangeTokenSelector() } + Box( + Modifier + .align(Alignment.CenterEnd) + .height(TangemTheme.dimens.size116) + .width(TangemTheme.dimens.size102) + .clickable( + indication = rememberRipple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + ) { onChangeTokenClick() }, + ) } } } @@ -114,55 +114,55 @@ fun TransactionCardEmpty( modifier: Modifier = Modifier, onChangeTokenClick: (() -> Unit)? = null, ) { - Card( - shape = RoundedCornerShape(TangemTheme.dimens.radius12), - backgroundColor = TangemTheme.colors.background.primary, - elevation = TangemTheme.dimens.elevation2, - modifier = modifier, + Box( + modifier = modifier + .background( + shape = RoundedCornerShape(TangemTheme.dimens.radius12), + color = TangemTheme.colors.background.primary, + ) + .fillMaxSize(), ) { - Box(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxWidth(), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.Start, - ) { - Header( - balance = stringResource(id = R.string.swapping_token_not_available), - type = type, - ) + Column( + modifier = Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start, + ) { + Header( + balance = stringResource(id = R.string.swapping_token_not_available), + type = type, + ) - Content( - type = type, - amountEquivalent = amountEquivalent, - textFieldValue = textFieldValue, - priceImpact = null, - ) - } + Content( + type = type, + amountEquivalent = amountEquivalent, + textFieldValue = textFieldValue, + priceImpact = null, + ) + } - Box(modifier = Modifier.align(Alignment.BottomEnd)) { - Token( - tokenIconUrl = "", - tokenCurrency = "", - iconPlaceholder = R.drawable.ic_no_token_44, - ) - } + Box(modifier = Modifier.align(Alignment.BottomEnd)) { + Token( + tokenIconUrl = "", + tokenCurrency = "", + iconPlaceholder = R.drawable.ic_no_token_44, + ) + } - if (onChangeTokenClick != null) { - Box(modifier = Modifier.align(Alignment.CenterEnd)) { - ChangeTokenSelector() - } - Box( - Modifier - .align(Alignment.CenterEnd) - .height(TangemTheme.dimens.size116) - .width(TangemTheme.dimens.size102) - .clickable( - indication = rememberRipple(bounded = false), - interactionSource = remember { MutableInteractionSource() }, - ) { onChangeTokenClick() }, - ) + if (onChangeTokenClick != null) { + Box(modifier = Modifier.align(Alignment.CenterEnd)) { + ChangeTokenSelector() } + Box( + Modifier + .align(Alignment.CenterEnd) + .height(TangemTheme.dimens.size116) + .width(TangemTheme.dimens.size102) + .clickable( + indication = rememberRipple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + ) { onChangeTokenClick() }, + ) } } } @@ -372,6 +372,7 @@ private fun TokenIcon( color = iconBackgroundColor, shape = TangemTheme.shapes.roundedCorners8, ) + .clip(TangemTheme.shapes.roundedCorners8) val data = tokenIconUrl.ifEmpty { iconPlaceholder } From 95510a665c03ecc484d511d0bf27340af5fc853b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 19:09:13 +0300 Subject: [PATCH 096/139] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../feature/wallet/presentation/router/DefaultWalletRouter.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 2a612cd92b..6ef8ce45d1 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 2a612cd92b1c78b99c917d0fe66342831f801e5e +Subproject commit 6ef8ce45d183905b5752e2d33c1d8bf2f4bcace6 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index e5ad37394c..8807297f9f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -63,6 +63,7 @@ internal class DefaultWalletRouter( val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) WalletScreen(state = viewModel.uiState) } From 95c2457832db7d5bc60ce43ecdd50b89c4741640 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 30 Nov 2023 18:50:17 +0200 Subject: [PATCH 097/139] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/features/BaseFragment.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt index e7aa8ff258..b2e20fad50 100644 --- a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt @@ -75,11 +75,13 @@ interface FragmentOnBackPressedHandler { @SuppressLint("FragmentBackPressedCallback") fun Fragment.addBackPressHandler(handler: FragmentOnBackPressedHandler) { - requireActivity().onBackPressedDispatcher.addCallback { - handler.handleOnBackPressed() - } + requireActivity().onBackPressedDispatcher.addCallback( + owner = this, + onBackPressed = { handler.handleOnBackPressed() } + ) view?.findViewById(R.id.toolbar)?.setNavigationOnClickListener { handler.handleOnBackPressed() } -} \ No newline at end of file +} + From 11748e9d192d4d17d7d7fa23854852da599a5b1d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Dec 2023 16:14:46 +0300 Subject: [PATCH 098/139] Updated on 2026-08-14 --- .../com/tangem/tap/features/BaseFragment.kt | 5 ++- .../swap/ui/SwapPermissionBottomSheet.kt | 5 --- .../feature/swap/viewmodels/SwapViewModel.kt | 32 ++++++++++++------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt index b2e20fad50..2c5ddc63f2 100644 --- a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt @@ -77,11 +77,10 @@ interface FragmentOnBackPressedHandler { fun Fragment.addBackPressHandler(handler: FragmentOnBackPressedHandler) { requireActivity().onBackPressedDispatcher.addCallback( owner = this, - onBackPressed = { handler.handleOnBackPressed() } + onBackPressed = { handler.handleOnBackPressed() }, ) view?.findViewById(R.id.toolbar)?.setNavigationOnClickListener { handler.handleOnBackPressed() } -} - +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt index 39ecc48a11..fdf6c22c6d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference @@ -44,10 +43,6 @@ private fun SwapPermissionBottomSheetContent(content: GivePermissionBottomSheetC .padding(horizontal = TangemTheme.dimens.spacing16), horizontalAlignment = Alignment.CenterHorizontally, ) { - Hand() - - SpacerH10() - Box(modifier = Modifier.fillMaxWidth()) { Text( modifier = Modifier.align(Alignment.Center), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 4122723684..76f1473044 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -200,14 +200,17 @@ internal class SwapViewModel @Inject constructor( toToken: CryptoCurrencyStatus, amount: String, toProvidersList: List, + isSilent: Boolean = false, ) { singleTaskScheduler.cancelTask() - uiState = stateBuilder.createQuotesLoadingState( - uiState, - fromToken.currency, - toToken.currency, - initialCryptoCurrency.id.value, - ) + if (!isSilent) { + uiState = stateBuilder.createQuotesLoadingState( + uiState, + fromToken.currency, + toToken.currency, + initialCryptoCurrency.id.value, + ) + } singleTaskScheduler.scheduleTask( viewModelScope, loadQuotesTask( @@ -219,7 +222,7 @@ internal class SwapViewModel @Inject constructor( ) } - private fun startLoadingQuotesFromLastState() { + private fun startLoadingQuotesFromLastState(isSilent: Boolean = false) { val fromCurrency = dataState.fromCryptoCurrency val toCurrency = dataState.toCryptoCurrency val amount = dataState.amount @@ -228,6 +231,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromCurrency, toToken = toCurrency, amount = amount, + isSilent = isSilent, toProvidersList = findSwapProviders(fromCurrency, toCurrency), ) } @@ -429,6 +433,12 @@ internal class SwapViewModel @Inject constructor( private fun givePermissionsToSwap() { viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { + val feeForPermission = when (val fee = dataState.approveDataModel?.fee) { + TxFeeState.Empty -> error("Fee should not be Empty") + is TxFeeState.MultipleFeeState -> fee.priorityFee + is TxFeeState.SingleFeeState -> fee.fee + null -> error("Fee should not be null") + } swapInteractor.givePermissionToSwap( networkId = dataState.networkId, permissionOptions = PermissionOptions( @@ -444,9 +454,7 @@ internal class SwapViewModel @Inject constructor( approveType = requireNotNull(dataState.approveType) { "uiState.permissionState should not be null" }.toDomainApproveType(), - txFee = requireNotNull(dataState.selectedFee) { - "dataState.selectedFee shouldn't be null" - }, + txFee = feeForPermission, spenderAddress = requireNotNull(dataState.approveDataModel?.spenderAddress) { "dataState.approveDataModel.spenderAddress shouldn't be null" }, @@ -664,9 +672,9 @@ internal class SwapViewModel @Inject constructor( singleTaskScheduler.cancelTask() analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) uiState = stateBuilder.showPermissionBottomSheet(uiState) { - startLoadingQuotesFromLastState() + startLoadingQuotesFromLastState(isSilent = true) analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked) - stateBuilder.dismissBottomSheet(uiState) + uiState = stateBuilder.dismissBottomSheet(uiState) } }, onAmountSelected = { onAmountSelected(it) }, From 92ee3c0273c22f89556186cfb92aa288ad3d608e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Dec 2023 16:35:36 +0200 Subject: [PATCH 099/139] Updated on 2026-08-14 --- .../local/preferences/PreferencesKeys.kt | 2 ++ .../swap/DefaultSwapTransactionRepository.kt | 34 +++++++++++++++++++ .../DefaultInitialToCurrencyResolver.kt | 33 ++++++++++++++++++ .../swap/domain/InitialToCurrencyResolver.kt | 15 ++++++++ .../feature/swap/domain/SwapInteractor.kt | 5 +++ .../feature/swap/domain/SwapInteractorImpl.kt | 21 +++++++++++- .../swap/domain/SwapTransactionRepository.kt | 4 +++ .../swap/domain/di/SwapDomainModule.kt | 14 ++++++++ .../domain/SavedLastSwappedCryptoCurrency.kt | 6 ++++ .../feature/swap/viewmodels/SwapViewModel.kt | 13 +++---- 10 files changed, 140 insertions(+), 7 deletions(-) create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 65be83c11a..213d153c62 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -36,6 +36,8 @@ object PreferencesKeys { val SWAP_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "swapTransactions") } val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") } + + val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index 63c8aecb72..e6cb59c754 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -3,9 +3,11 @@ package com.tangem.feature.swap import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList +import com.tangem.datasource.local.preferences.utils.getObjectListSync import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.SwapTransactionRepository +import com.tangem.feature.swap.domain.models.domain.SavedLastSwappedCryptoCurrency import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel import com.tangem.utils.extensions.addOrReplace @@ -128,6 +130,38 @@ class DefaultSwapTransactionRepository( } } + override suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String? { + val lastSwappedCurrencies = appPreferencesStore.getObjectListSync( + key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY, + ) + + return lastSwappedCurrencies.find { userWalletId.stringValue == it.userWalletId }?.cryptoCurrencyId + } + + override suspend fun storeLastSwappedCryptoCurrencyId( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + ) { + appPreferencesStore.editData { mutablePreferences -> + val lastSwappedCryptoCurrencies: List? = mutablePreferences.getObjectList( + key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY, + ) + + val newList = if (lastSwappedCryptoCurrencies != null) { + lastSwappedCryptoCurrencies.filter { + it.userWalletId != userWalletId.stringValue + } + SavedLastSwappedCryptoCurrency(userWalletId.stringValue, cryptoCurrencyId.value) + } else { + listOf(SavedLastSwappedCryptoCurrency(userWalletId.stringValue, cryptoCurrencyId.value)) + } + + mutablePreferences.setObjectList( + key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY, + value = newList, + ) + } + } + private fun SavedSwapTransactionListModel.checkId( checkUserWalletId: UserWalletId, fromCurrencyId: CryptoCurrency.ID, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt new file mode 100644 index 0000000000..42e6f2331b --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt @@ -0,0 +1,33 @@ +package com.tangem.feature.swap.domain + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress +import java.math.BigDecimal + +internal class DefaultInitialToCurrencyResolver( + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val swapTransactionRepository: SwapTransactionRepository, +) : InitialToCurrencyResolver { + + override suspend fun tryGetFromCache( + initialCryptoCurrency: CryptoCurrency, + state: TokensDataStateExpress, + ): CryptoCurrencyStatus? { + val selectedId = getSelectedWalletSyncUseCase().getOrNull() ?: return null + val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(selectedId.walletId) ?: return null + + return if (id != initialCryptoCurrency.id.value) { + state.toGroup.available.find { it.currencyStatus.currency.id.value == id }?.currencyStatus + } else { + null + } + } + + override fun tryGetWithMaxAmount(state: TokensDataStateExpress): CryptoCurrencyStatus? { + return state.toGroup.available.maxByOrNull { + it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO + }?.currencyStatus + } +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt new file mode 100644 index 0000000000..da0b0f1b37 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt @@ -0,0 +1,15 @@ +package com.tangem.feature.swap.domain + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress + +interface InitialToCurrencyResolver { + + suspend fun tryGetFromCache( + initialCryptoCurrency: CryptoCurrency, + state: TokensDataStateExpress, + ): CryptoCurrencyStatus? + + fun tryGetWithMaxAmount(state: TokensDataStateExpress): CryptoCurrencyStatus? +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 01193a0ea4..c75b56fd63 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -85,4 +85,9 @@ interface SwapInteractor { fun isAvailableToSwap(networkId: String): Boolean fun getSelectedWallet(): UserWallet? + + suspend fun selectInitialCurrencyToSwap( + initialCryptoCurrency: CryptoCurrency, + state: TokensDataStateExpress, + ): CryptoCurrencyStatus? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 685e925ce8..29e2aacdb7 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -43,7 +43,6 @@ internal class SwapInteractorImpl @Inject constructor( private val transactionManager: TransactionManager, private val userWalletManager: UserWalletManager, private val repository: SwapRepository, - private val swapTransactionRepository: SwapTransactionRepository, private val allowPermissionsHandler: AllowPermissionsHandler, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, @@ -51,6 +50,8 @@ internal class SwapInteractorImpl @Inject constructor( private val sendTransactionUseCase: SendTransactionUseCase, private val quotesRepository: QuotesRepository, private val dispatcher: CoroutineDispatcherProvider, + private val swapTransactionRepository: SwapTransactionRepository, + private val initialToCurrencyResolver: InitialToCurrencyResolver, ) : SwapInteractor { private val getFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { @@ -427,6 +428,7 @@ internal class SwapInteractorImpl @Inject constructor( ) return when (result) { is SendTxResult.Success -> { + storeLastCryptoCurrencyId(currencyToGet) TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, @@ -503,6 +505,7 @@ internal class SwapInteractorImpl @Inject constructor( swapDataModel = exchangeData.dataModel, timestamp = timestamp, ) + storeLastCryptoCurrencyId(currencyToGet.currency) TxState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, @@ -564,6 +567,13 @@ internal class SwapInteractorImpl @Inject constructor( ) } + private suspend fun storeLastCryptoCurrencyId(cryptoCurrency: CryptoCurrency) { + swapTransactionRepository.storeLastSwappedCryptoCurrencyId( + UserWalletId(userWalletManager.getWalletId()), + cryptoCurrency.id, + ) + } + @Deprecated("used in old swap mechanism") override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount { return SwapAmount(token.value.amount ?: BigDecimal.ZERO, getTokenDecimals(token.currency)) @@ -574,6 +584,15 @@ internal class SwapInteractorImpl @Inject constructor( return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId) } + override suspend fun selectInitialCurrencyToSwap( + initialCryptoCurrency: CryptoCurrency, + state: TokensDataStateExpress, + ): CryptoCurrencyStatus? { + return initialToCurrencyResolver.tryGetFromCache(initialCryptoCurrency, state) + ?: initialToCurrencyResolver.tryGetWithMaxAmount(state) + ?: state.toGroup.available.firstOrNull()?.currencyStatus + } + @Deprecated("used in old swap mechanism") private fun getTangemFee(): Double { return repository.getTangemFee() diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt index d89914d537..2db58ee01d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt @@ -26,4 +26,8 @@ interface SwapTransactionRepository { toCryptoCurrencyId: CryptoCurrency.ID, txId: String, ) + + suspend fun storeLastSwappedCryptoCurrencyId(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID) + + suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index d72ebef25d..f6bf9093c9 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -40,6 +40,7 @@ class SwapDomainModule { swapTransactionRepository: SwapTransactionRepository, walletManagersFacade: WalletManagersFacade, coroutineDispatcherProvider: CoroutineDispatcherProvider, + initialToCurrencyResolver: InitialToCurrencyResolver, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -53,6 +54,7 @@ class SwapDomainModule { walletManagersFacade = walletManagersFacade, dispatcher = coroutineDispatcherProvider, swapTransactionRepository = swapTransactionRepository, + initialToCurrencyResolver = initialToCurrencyResolver, ) } @@ -124,6 +126,18 @@ class SwapDomainModule { walletManagersFacade = walletManagersFacade, ) } + + @Provides + @Singleton + fun provideInitialToCurrencyResolver( + @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + swapTransactionRepository: SwapTransactionRepository, + ): InitialToCurrencyResolver { + return DefaultInitialToCurrencyResolver( + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + swapTransactionRepository = swapTransactionRepository, + ) + } } @Qualifier diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt new file mode 100644 index 0000000000..2db24c66eb --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt @@ -0,0 +1,6 @@ +package com.tangem.feature.swap.domain.models.domain + +data class SavedLastSwappedCryptoCurrency( + val userWalletId: String, + val cryptoCurrencyId: String, +) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 76f1473044..83fb774ca3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -153,7 +153,13 @@ internal class SwapViewModel @Inject constructor( swapInteractor.getTokensDataState(initialCryptoCurrency) }.onSuccess { state -> updateTokensState(state) - applyInitialTokenChoice(state, selectInitialCurrencyToSwap(state)) + applyInitialTokenChoice( + state, + swapInteractor.selectInitialCurrencyToSwap( + initialCryptoCurrency, + state, + ), + ) }.onFailure { Timber.tag(loggingTag).e(it) } @@ -182,11 +188,6 @@ internal class SwapViewModel @Inject constructor( } } - private fun selectInitialCurrencyToSwap(state: TokensDataStateExpress): CryptoCurrencyStatus? { - // todo add algorithm to select initial currency - return state.toGroup.available.firstOrNull()?.currencyStatus - } - private fun updateTokensState(dataState: TokensDataStateExpress) { val tokensDataState = if (!isOrderReversed) dataState.toGroup else dataState.fromGroup uiState = stateBuilder.addTokensToState( From d22cc0fe8b61ea19122902c728774029197aec85 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Dec 2023 17:24:50 +0300 Subject: [PATCH 100/139] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 6 +- .../analytics/TokenExchangeAnalyticsEvent.kt | 44 +++++++++++++ .../swap/DefaultSwapTransactionRepository.kt | 4 +- .../state/factory/TokenDetailsStateFactory.kt | 8 +-- ...enDetailsSwapTransactionsStateConverter.kt | 63 +++++++++++-------- .../viewmodels/ExchangeStatusFactory.kt | 8 ++- .../viewmodels/TokenDetailsClickIntents.kt | 3 +- .../viewmodels/TokenDetailsViewModel.kt | 26 +++++++- 9 files changed, 124 insertions(+), 42 deletions(-) create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenExchangeAnalyticsEvent.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 8805ebe16d..13ae7d8b04 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -198,9 +198,9 @@ Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами Выберите провайдера Чтобы узнать причину, посетите сайт провайдера - Операция не выполнена провайдером + Чтобы вернуть ваши деньги, посетите сайт провайдера Посетите сайт провайдера для проверки - Провайдер: требуется верификация + Провайдер запрашивает прохождение верификации Список токенов в вашем кошельке Получение наилучших курсов... Провайдер diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 62304b3e12..44af8e6ed5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -198,18 +198,22 @@ Choose provider Estimated amount Exchange by %s - Visit provider’s website to see why + Visit provider’s website to refund your money Operation failed by provider Visit provider’s website for verification KYC verification required by provider Confirmed Confirming + Confirming… Exchanged Exchanging + Exchanging… Failed Deposit received Awaiting deposit + Awaiting deposit… Sending to you + Sending to you… Sent Provider-sourced data. Estimated amount subject to change. Exchange status diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenExchangeAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenExchangeAnalyticsEvent.kt new file mode 100644 index 0000000000..a795ccd2af --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenExchangeAnalyticsEvent.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.tokens.models.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +class TokenExchangeAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Token", event, params, null) { + + class CexTx(token: String) : TokenScreenAnalyticsEvent( + event = "Notice - ChangeNow Swap", + params = mapOf("Token" to token), + ) + + class CexTxOpened(token: String, status: String) : TokenScreenAnalyticsEvent( + event = "ChangeNow Swap Opened", + params = mapOf("Token" to token, "Status" to status), + ) + + class Verification(token: String) : TokenScreenAnalyticsEvent( + event = "Notice - KYC required", + params = mapOf("Token" to token), + ) + + class Fail(token: String) : TokenScreenAnalyticsEvent( + event = "Notice - Operation Fail", + params = mapOf("Token" to token), + ) + + class GoToProviderStatus(token: String) : TokenScreenAnalyticsEvent( + event = "Button - Go To Provider", + params = mapOf("Token" to token, "Place" to "Status"), + ) + + class GoToProviderKYC(token: String) : TokenScreenAnalyticsEvent( + event = "Button - Go To Provider", + params = mapOf("Token" to token, "Place" to "KYC"), + ) + + class GoToProviderFail(token: String) : TokenScreenAnalyticsEvent( + event = "Button - Go To Provider", + params = mapOf("Token" to token, "Place" to "Fail"), + ) +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index 63c8aecb72..d60d26a19e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -97,11 +97,11 @@ class DefaultSwapTransactionRepository( ) } ?.transactions - ?.dropWhile { it.txId == txId } + ?.filterNot { it.txId == txId } val editedList = if (tokenTransactions.isNullOrEmpty()) { - savedList?.dropWhile { + savedList?.filterNot { it.checkId( checkUserWalletId = userWalletId, fromCurrencyId = fromCryptoCurrencyId, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 368164ce46..44e7b1f2c4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -22,6 +22,7 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter @@ -243,14 +244,13 @@ internal class TokenDetailsStateFactory( return state.copy(notifications = notificationConverter.removeRentInfo(state)) } - fun getStateWithExchangeStatusBottomSheet(txId: String): TokenDetailsState { - val state = currentStateProvider() - return state.copy( + fun getStateWithExchangeStatusBottomSheet(swapTxState: SwapTransactionsState): TokenDetailsState { + return currentStateProvider().copy( bottomSheetConfig = TangemBottomSheetConfig( isShow = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = ExchangeStatusBottomSheetConfig( - value = state.swapTxs.first { it.txId == txId }, + value = swapTxState, ), ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index c0be53eb94..39d60d2436 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -1,14 +1,16 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.common.Provider +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.toDateFormat import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState @@ -26,6 +28,8 @@ import java.math.BigDecimal internal class TokenDetailsSwapTransactionsStateConverter( private val clickIntents: TokenDetailsClickIntents, + private val cryptoCurrency: CryptoCurrency, + private val analyticsEventsHandlerProvider: Provider, appCurrencyProvider: Provider, ) : Converter> { @@ -89,8 +93,13 @@ internal class TokenDetailsSwapTransactionsStateConverter( fromCryptoSymbol = fromCurrency.currency.symbol, fromFiatAmount = getFiatAmount(fromFiatAmount), fromCurrencyIcon = iconStateConverter.convert(fromCurrency), - onClick = { clickIntents.onSwapTransactionClick(transaction.txId) }, - onGoToProviderClick = { clickIntents.onGoToProviderClick(transaction.status?.txUrl.orEmpty()) }, + onClick = { clickIntents.onSwapTransactionClick(transaction.txId, transaction.status?.status) }, + onGoToProviderClick = { + analyticsEventsHandlerProvider().send( + TokenExchangeAnalyticsEvent.GoToProviderStatus(cryptoCurrency.symbol), + ) + clickIntents.onGoToProviderClick(url = transaction.status?.txUrl.orEmpty()) + }, ), ) } @@ -116,11 +125,27 @@ internal class TokenDetailsSwapTransactionsStateConverter( private fun getNotification(status: ExchangeStatus?, txUrl: String?): ExchangeStatusNotifications? { if (txUrl == null) return null return when (status) { - ExchangeStatus.Failed -> ExchangeStatusNotifications.Failed { - clickIntents.onGoToProviderClick(txUrl) + ExchangeStatus.Failed -> { + analyticsEventsHandlerProvider().send( + TokenExchangeAnalyticsEvent.Fail(cryptoCurrency.symbol), + ) + ExchangeStatusNotifications.Failed { + analyticsEventsHandlerProvider().send( + TokenExchangeAnalyticsEvent.GoToProviderFail(cryptoCurrency.symbol), + ) + clickIntents.onGoToProviderClick(txUrl) + } } - ExchangeStatus.Verifying -> ExchangeStatusNotifications.NeedVerification { - clickIntents.onGoToProviderClick(txUrl) + ExchangeStatus.Verifying -> { + analyticsEventsHandlerProvider().send( + TokenExchangeAnalyticsEvent.Verification(cryptoCurrency.symbol), + ) + ExchangeStatusNotifications.NeedVerification { + analyticsEventsHandlerProvider().send( + TokenExchangeAnalyticsEvent.GoToProviderKYC(cryptoCurrency.symbol), + ) + clickIntents.onGoToProviderClick(txUrl) + } } else -> null } @@ -150,10 +175,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( private fun waitStep(isNew: Boolean, isNewDone: Boolean) = ExchangeStatusState( status = ExchangeStatus.New, text = when { - isNew -> combinedReference( - TextReference.Res(R.string.express_exchange_status_receiving), - TextReference.Str(STATUS_ACTIVE_DOTS), - ) + isNew -> TextReference.Res(R.string.express_exchange_status_receiving_active) else -> TextReference.Res(R.string.express_exchange_status_receiving) }, isActive = isNew, @@ -163,10 +185,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( private fun confirmStep(isConfirming: Boolean, isConfirmingDone: Boolean) = ExchangeStatusState( status = ExchangeStatus.Confirming, text = when { - isConfirming -> combinedReference( - TextReference.Res(R.string.express_exchange_status_confirming), - TextReference.Str(STATUS_ACTIVE_DOTS), - ) + isConfirming -> TextReference.Res(R.string.express_exchange_status_confirming_active) isConfirmingDone -> TextReference.Res(R.string.express_exchange_status_confirmed) else -> TextReference.Res(R.string.express_exchange_status_confirming) }, @@ -195,10 +214,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( else -> ExchangeStatusState( status = ExchangeStatus.Exchanging, text = when { - isExchanging -> combinedReference( - TextReference.Res(R.string.express_exchange_status_exchanging), - TextReference.Str(STATUS_ACTIVE_DOTS), - ) + isExchanging -> TextReference.Res(R.string.express_exchange_status_exchanging_active) isExchangingDone -> TextReference.Res(R.string.express_exchange_status_exchanged) else -> TextReference.Res(R.string.express_exchange_status_exchanging) }, @@ -210,18 +226,11 @@ internal class TokenDetailsSwapTransactionsStateConverter( private fun sendStep(isSending: Boolean, isSendingDone: Boolean) = ExchangeStatusState( status = ExchangeStatus.Sending, text = when { - isSending -> combinedReference( - TextReference.Res(R.string.express_exchange_status_sending), - TextReference.Str(STATUS_ACTIVE_DOTS), - ) + isSending -> TextReference.Res(R.string.express_exchange_status_sending_active) isSendingDone -> TextReference.Res(R.string.express_exchange_status_sent) else -> TextReference.Res(R.string.express_exchange_status_sending) }, isActive = isSending, isDone = isSendingDone, ) - - private companion object { - private const val STATUS_ACTIVE_DOTS = "…" - } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index d92306d03c..b1441a921a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels import arrow.core.getOrElse import com.tangem.common.Provider +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency @@ -35,19 +36,22 @@ internal class ExchangeStatusFactory( private val dispatchers: CoroutineDispatcherProvider, private val clickIntents: TokenDetailsClickIntents, private val appCurrencyProvider: Provider, + private val analyticsEventsHandlerProvider: Provider, private val userWalletId: UserWalletId, - private val cryptoCurrencyId: CryptoCurrency.ID, + private val cryptoCurrency: CryptoCurrency, ) { private val swapTransactionsStateConverter by lazy { TokenDetailsSwapTransactionsStateConverter( clickIntents = clickIntents, + cryptoCurrency = cryptoCurrency, appCurrencyProvider = appCurrencyProvider, + analyticsEventsHandlerProvider = analyticsEventsHandlerProvider, ) } operator fun invoke() = combine( - flow = swapTransactionRepository.getTransactions(userWalletId, cryptoCurrencyId), + flow = swapTransactionRepository.getTransactions(userWalletId, cryptoCurrency.id), flow2 = getWalletCryptoCurrencies().conflate(), ) { savedTransactions, cryptoCurrenciesStatusList -> innerLoadSwapState( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 95093f7fdb..64a0d6dd2e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus interface TokenDetailsClickIntents { @@ -39,7 +40,7 @@ interface TokenDetailsClickIntents { fun onCloseRentInfoNotification() - fun onSwapTransactionClick(txId: String) + fun onSwapTransactionClick(txId: String, status: ExchangeStatus?) fun onGoToProviderClick(url: String) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 126298c471..a730e6edd2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase @@ -35,9 +36,11 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.domain.SwapRepository import com.tangem.feature.swap.domain.SwapTransactionRepository +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.features.tokendetails.impl.R import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -117,8 +120,9 @@ internal class TokenDetailsViewModel @Inject constructor( dispatchers = dispatchers, clickIntents = this, appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, + analyticsEventsHandlerProvider = Provider { analyticsEventsHandler }, userWalletId = userWalletId, - cryptoCurrencyId = cryptoCurrency.id, + cryptoCurrency = cryptoCurrency, ) } @@ -212,6 +216,9 @@ internal class TokenDetailsViewModel @Inject constructor( swapTxStatusTaskScheduler.cancelTask() exchangeStatusFactory.invoke() .onEach { swapTxs -> + if (swapTxs.isNotEmpty()) { + analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.CexTx(cryptoCurrency.symbol)) + } swapTxStatusTaskScheduler.scheduleTask( viewModelScope, PeriodicTask( @@ -533,8 +540,21 @@ internal class TokenDetailsViewModel @Inject constructor( uiState = stateFactory.getStateWithRemovedRentNotification() } - override fun onSwapTransactionClick(txId: String) { - uiState = stateFactory.getStateWithExchangeStatusBottomSheet(txId) + override fun onSwapTransactionClick(txId: String, status: ExchangeStatus?) { + val swapTxState = uiState.swapTxs.first { it.txId == txId } + analyticsEventsHandler.send( + TokenExchangeAnalyticsEvent.CexTxOpened(cryptoCurrency.symbol, status?.name.orEmpty()), + ) + when (swapTxState.notification.value) { + is ExchangeStatusNotifications.NeedVerification -> { + analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.Verification(cryptoCurrency.symbol)) + } + is ExchangeStatusNotifications.Failed -> { + analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.Fail(cryptoCurrency.symbol)) + } + else -> Unit + } + uiState = stateFactory.getStateWithExchangeStatusBottomSheet(swapTxState) } override fun onGoToProviderClick(url: String) { From 34ffa9a1e719612fe0e7fac58bea264d316abde1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Dec 2023 19:40:54 +0400 Subject: [PATCH 101/139] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../java/com/tangem/tap/TapApplication.kt | 6 ++++ .../amplitude/AmplitudeAnalyticsHandler.kt | 4 +-- .../appsFlyer/AppsFlyerAnalyticsHandler.kt | 4 +-- .../firebase/FirebaseAnalyticsHandler.kt | 4 +-- core/analytics/build.gradle.kts | 9 +++--- .../core/analytics/models/AnalyticsEvent.kt | 5 ++- .../analytics/models/OneTimeAnalyticsEvent.kt | 6 ++++ core/analytics/src/main/AndroidManifest.xml | 2 -- .../core/analytics/api/EventFilterApi.kt | 2 +- .../core/analytics/api/EventHandlerApi.kt | 6 ++-- .../core/analytics/di/AnalyticsModule.kt | 9 +++++- .../analytics/filter/OneTimeEventFilter.kt | 30 +++++++++++++++++ .../repository/AnalyticsRepository.kt | 8 +++++ .../local/preferences/PreferencesKeys.kt | 2 ++ data/analytics/.gitignore | 1 + data/analytics/build.gradle.kts | 32 +++++++++++++++++++ .../analytics/DefaultAnalyticsRepository.kt | 27 ++++++++++++++++ .../data/analytics/di/AnalyticsDataModule.kt | 19 +++++++++++ settings.gradle.kts | 2 +- 20 files changed, 158 insertions(+), 21 deletions(-) create mode 100644 core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimeAnalyticsEvent.kt delete mode 100644 core/analytics/src/main/AndroidManifest.xml create mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt create mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt create mode 100644 data/analytics/.gitignore create mode 100644 data/analytics/build.gradle.kts create mode 100644 data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt create mode 100644 data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e3f7a4fcde..a3a05911c7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { implementation(projects.data.tokens) implementation(projects.data.txhistory) implementation(projects.data.wallets) + implementation(projects.data.analytics) /** Features */ implementation(projects.features.onboarding) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 420a6ff091..0aecd2719a 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -14,6 +14,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.api.common.MoshiConverter @@ -216,6 +217,9 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var sendFeatureToggles: SendFeatureToggles + + @Inject + lateinit var oneTimeEventFilter: OneTimeEventFilter // endregion Injected override fun onCreate() { @@ -353,6 +357,8 @@ internal class TapApplication : Application(), ImageLoaderFactory { factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder()) factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder()) + factory.addFilter(oneTimeEventFilter) + val buildData = AnalyticsHandlerBuilder.Data( application = application, config = config, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt index a1daeb7ccf..5eed564c97 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt @@ -9,8 +9,8 @@ class AmplitudeAnalyticsHandler( override fun id(): String = ID - override fun send(event: String, params: Map) { - client.logEvent(event, params) + override fun send(eventId: String, params: Map) { + client.logEvent(eventId, params) } companion object { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt index c4b12fc278..710c7308e6 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt @@ -12,8 +12,8 @@ class AppsFlyerAnalyticsHandler( override fun id(): String = ID - override fun send(event: String, params: Map) { - client.logEvent(event, params) + override fun send(eventId: String, params: Map) { + client.logEvent(eventId, params) } override fun send(event: AnalyticsEvent) { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt index e445597e8f..10b399382c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt @@ -14,8 +14,8 @@ class FirebaseAnalyticsHandler( override fun id(): String = ID - override fun send(event: String, params: Map) { - client.logEvent(event, params) + override fun send(eventId: String, params: Map) { + client.logEvent(eventId, params) } override fun send(event: AnalyticsEvent) { diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index 29b4cfb591..663fb0f33e 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -1,19 +1,18 @@ plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.jvm) alias(deps.plugins.kotlin.kapt) - alias(deps.plugins.hilt.android) id("configuration") } dependencies { /** DI */ - implementation(deps.hilt.android) + implementation(deps.hilt.core) kapt(deps.hilt.kapt) /** Core shouldn't depends on core, but in case with utils and logging its necessary */ implementation(projects.core.utils) - implementation(projects.core.analytics.models) + + implementation(deps.kotlin.coroutines) } \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt index e60b14d1ee..025a6c1038 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt @@ -8,4 +8,7 @@ open class AnalyticsEvent( val event: String, var params: Map = mapOf(), val error: Throwable? = null, -) \ No newline at end of file +) { + + val id: String = "[$category] $event" +} \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimeAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimeAnalyticsEvent.kt new file mode 100644 index 0000000000..82d9991c71 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimeAnalyticsEvent.kt @@ -0,0 +1,6 @@ +package com.tangem.core.analytics.models + +interface OneTimeAnalyticsEvent { + + val oneTimeEventId: String +} \ No newline at end of file diff --git a/core/analytics/src/main/AndroidManifest.xml b/core/analytics/src/main/AndroidManifest.xml deleted file mode 100644 index 9270c6ee97..0000000000 --- a/core/analytics/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt index 4de7e4aee8..dc94652208 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt @@ -16,7 +16,7 @@ interface AnalyticsEventFilter { * An internal filter check that, on external or internal conditions, recognizes the possibility of * sending an event. */ - fun canBeSent(event: AnalyticsEvent): Boolean + suspend fun canBeSent(event: AnalyticsEvent): Boolean /** * Performs a check to see if the event can be dispatched by a specific handler diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt index 0a5f3179de..7bedfebff3 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt @@ -12,13 +12,11 @@ interface AnalyticsEventHandler { interface AnalyticsHandler : AnalyticsEventHandler { fun id(): String - fun send(event: String, params: Map = emptyMap()) + fun send(eventId: String, params: Map = emptyMap()) override fun send(event: AnalyticsEvent) { - send(prepareEventString(event), event.params) + send(event.id, event.params) } - - fun prepareEventString(event: AnalyticsEvent): String = "[${event.category}] ${event.event}" } interface ErrorEventHandler { diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt b/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt index 3b12478c96..b4bb704732 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt @@ -2,6 +2,8 @@ package com.tangem.core.analytics.di import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.filter.OneTimeEventFilter +import com.tangem.core.analytics.repository.AnalyticsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -10,11 +12,16 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class AnalyticsModule { +internal object AnalyticsModule { @Singleton @Provides fun provideAnalyticsHandler(): AnalyticsEventHandler { return Analytics // todo replace after refactoring calling Analytics in whole project } + + @Provides + fun provideOneTimeEventFilter(analyticsRepository: AnalyticsRepository): OneTimeEventFilter { + return OneTimeEventFilter(analyticsRepository) + } } \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt b/core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt new file mode 100644 index 0000000000..c224eff75b --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt @@ -0,0 +1,30 @@ +package com.tangem.core.analytics.filter + +import com.tangem.core.analytics.api.AnalyticsEventFilter +import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.OneTimeAnalyticsEvent +import com.tangem.core.analytics.repository.AnalyticsRepository + +class OneTimeEventFilter( + private val analyticsRepository: AnalyticsRepository, +) : AnalyticsEventFilter { + + override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is OneTimeAnalyticsEvent + + override suspend fun canBeSent(event: AnalyticsEvent): Boolean { + if (event !is OneTimeAnalyticsEvent) return true + + val isSent = analyticsRepository.checkIsEventSent(event.oneTimeEventId) + + if (!isSent) { + analyticsRepository.setIsEventSent(event.oneTimeEventId) + } + + return !isSent + } + + override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean { + return canBeAppliedTo(event) + } +} \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt b/core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt new file mode 100644 index 0000000000..859220527a --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt @@ -0,0 +1,8 @@ +package com.tangem.core.analytics.repository + +interface AnalyticsRepository { + + suspend fun checkIsEventSent(eventId: String): Boolean + + suspend fun setIsEventSent(eventId: String) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 6eda0886c9..0ad9c10237 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -34,6 +34,8 @@ object PreferencesKeys { val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") } val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") } + + val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/data/analytics/.gitignore b/data/analytics/.gitignore new file mode 100644 index 0000000000..3a48006eea --- /dev/null +++ b/data/analytics/.gitignore @@ -0,0 +1 @@ +/buld diff --git a/data/analytics/build.gradle.kts b/data/analytics/build.gradle.kts new file mode 100644 index 0000000000..258c49ea37 --- /dev/null +++ b/data/analytics/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.analytics" +} + +dependencies { + + /** Project - Analytics */ + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + + /** Project - Data */ + implementation(projects.core.datasource) + implementation(projects.data.common) + + /** AndroidX */ + implementation(deps.androidx.datastore) + + /** DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.kotlin.coroutines) + implementation(deps.timber) +} \ No newline at end of file diff --git a/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt b/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt new file mode 100644 index 0000000000..9235045bae --- /dev/null +++ b/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt @@ -0,0 +1,27 @@ +package com.tangem.data.analytics + +import com.tangem.core.analytics.repository.AnalyticsRepository +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectListSync + +internal class DefaultAnalyticsRepository( + private val appPreferencesStore: AppPreferencesStore, +) : AnalyticsRepository { + + override suspend fun checkIsEventSent(eventId: String): Boolean { + val sentEvents = appPreferencesStore + .getObjectListSync(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY) + + return eventId in sentEvents + } + + override suspend fun setIsEventSent(eventId: String) { + appPreferencesStore.editData { mutablePreferences -> + val sentEvents = mutablePreferences.getObject>(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY) + val updatedSentEvents = sentEvents.orEmpty() + eventId + + mutablePreferences.setObject(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY, updatedSentEvents) + } + } +} \ No newline at end of file diff --git a/data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt b/data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt new file mode 100644 index 0000000000..ed3e2b04ac --- /dev/null +++ b/data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt @@ -0,0 +1,19 @@ +package com.tangem.data.analytics.di + +import com.tangem.core.analytics.repository.AnalyticsRepository +import com.tangem.data.analytics.DefaultAnalyticsRepository +import com.tangem.datasource.local.preferences.AppPreferencesStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object AnalyticsDataModule { + + @Provides + fun provideAnalyticsRepository(appPreferencesStore: AppPreferencesStore): AnalyticsRepository { + return DefaultAnalyticsRepository(appPreferencesStore) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 59ac1ba906..1abc2626ae 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -128,7 +128,6 @@ include(":domain:app-theme:models") include(":domain:balance-hiding") include(":domain:balance-hiding:models") include(":domain:transaction") - // endregion Domain modules // region Data modules @@ -142,4 +141,5 @@ include(":data:source:preferences") include(":data:settings") include(":data:txhistory") include(":data:wallets") +include(":data:analytics") // endregion Data modules \ No newline at end of file From cad55c940886b7502f5f1f93e57758612959e5db Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Dec 2023 13:12:53 +0400 Subject: [PATCH 102/139] Updated on 2026-08-14 --- .../BiometricUserWalletsListManager.kt | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index af472b3608..c285a15c80 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -67,15 +67,17 @@ internal class BiometricUserWalletsListManager( } } .map { - if (throwIfNotAllWalletsUnlocked && state.value.userWallets.any(UserWallet::isLocked)) { - Timber.e("Not all user wallets have been unlocked") + val userWallets = state.value.userWallets + + if (throwIfNotAllWalletsUnlocked && userWallets.any(UserWallet::isLocked)) { + Timber.e("Some user wallets remain locked") throw UserWalletsListError.NotAllUserWalletsUnlocked } val selectedUserWallet = selectedUserWalletSync if (selectedUserWallet == null || selectedUserWallet.isLocked) { - Timber.e("Unable to find selected user wallet") - throw UserWalletsListError.NoUserWalletSelected + findAndSetUnlockedUserWallet(userWallets) + ?: throw UserWalletsListError.NoUserWalletSelected } else { selectedUserWallet } @@ -280,13 +282,14 @@ internal class BiometricUserWalletsListManager( prevSelectedWalletId: UserWalletId?, userWallets: List, ): UserWalletId? { - val findUnlockedAndSet = { - userWallets.firstOrNull { !it.isLocked } - ?.walletId - ?.also { selectedUserWalletRepository.set(it) } - } + return prevSelectedWalletId + ?: (selectedUserWalletRepository.get() ?: findAndSetUnlockedUserWallet(userWallets)?.walletId) + } - return prevSelectedWalletId ?: (selectedUserWalletRepository.get() ?: findUnlockedAndSet()) + private fun findAndSetUnlockedUserWallet(userWallets: List): UserWallet? { + return userWallets + .firstOrNull { !it.isLocked } + ?.also { selectedUserWalletRepository.set(it.walletId) } } private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List) { From 23a5174a920ef6989dc89935060e0913ce6a8bc9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Dec 2023 14:31:41 +0300 Subject: [PATCH 103/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 3 +++ .../feature/swap/domain/models/ui/TxState.kt | 1 + .../swap/models/SwapSuccessStateHolder.kt | 3 ++- .../tangem/feature/swap/ui/StateBuilder.kt | 6 ++++-- .../feature/swap/ui/SwapSuccessScreen.kt | 20 +++++++------------ .../feature/swap/viewmodels/SwapViewModel.kt | 13 ++++++++++-- 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 29e2aacdb7..447edd8665 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -486,6 +486,8 @@ internal class SwapInteractorImpl @Inject constructor( network = currencyToSend.currency.network, ) + val externalUrl = (exchangeData.dataModel.transaction as? ExpressTransactionModel.CEX)?.externalTxUrl + return result.fold( ifLeft = { when (it) { @@ -519,6 +521,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend.currency.network.backendId, derivationPath, ).orEmpty(), + txExternalUrl = externalUrl, timestamp = timestamp, ) }, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt index 0a0f5b5b2a..a593888610 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt @@ -6,6 +6,7 @@ sealed class TxState { val fromAmount: String? = null, val toAmount: String? = null, val txAddress: String, + val txExternalUrl: String? = null, val timestamp: Long, ) : TxState() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt index 09d8728073..cbe749c6a8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt @@ -18,5 +18,6 @@ data class SwapSuccessStateHolder( val toTokenFiatAmount: TextReference, val fromTokenIconState: TokenIconState?, val toTokenIconState: TokenIconState?, - val onSecondaryButtonClick: () -> Unit, + val onExploreButtonClick: () -> Unit, + val onStatusButtonClick: () -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 4bab76b4a3..6a5f0a46f8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -585,7 +585,8 @@ internal class StateBuilder( fromAmount: BigDecimal, toAmount: BigDecimal, txUrl: String, - onSecondaryBtnClick: () -> Unit, + onExploreClick: () -> Unit, + onStatusClick: () -> Unit, ): SwapStateHolder { val providerState = uiState.providerState as ProviderState.Content val fromToken = requireNotNull((uiState.sendCardData as? SwapCardState.SwapCardData)?.token) @@ -612,7 +613,8 @@ internal class StateBuilder( toTokenFiatAmount = TextReference.Str(toFiatAmount), fromTokenIconState = fromTokenIconState, toTokenIconState = toTokenIconState, - onSecondaryButtonClick = onSecondaryBtnClick, + onExploreButtonClick = onExploreClick, + onStatusButtonClick = onStatusClick, ), ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 4c4221d250..d540514a81 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -7,9 +7,6 @@ import androidx.compose.material.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* @@ -20,7 +17,6 @@ import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImage import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType import com.tangem.feature.swap.models.SwapSuccessStateHolder @@ -44,7 +40,8 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) { textRes = R.string.common_close, txUrl = state.txUrl, showStatusButton = state.showStatusButton, - onExploreClick = state.onSecondaryButtonClick, + onExploreClick = state.onExploreButtonClick, + onStatusClick = state.onStatusButtonClick, onDoneClick = onBack, ) }, @@ -102,17 +99,16 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad } } +@Suppress("LongParameterList") @Composable private fun SwapSuccessScreenButtons( @StringRes textRes: Int, txUrl: String, showStatusButton: Boolean, onExploreClick: () -> Unit, + onStatusClick: () -> Unit, onDoneClick: () -> Unit, ) { - val hapticFeedback = LocalHapticFeedback.current - val context = LocalContext.current - Column( modifier = Modifier .background(TangemTheme.colors.background.secondary) @@ -131,10 +127,7 @@ private fun SwapSuccessScreenButtons( SecondaryButtonIconStart( text = stringResource(id = R.string.express_cex_status_button_title), iconResId = R.drawable.ic_arrow_top_right_24, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - context.shareText(txUrl) - }, + onClick = onStatusClick, modifier = Modifier.weight(1f), ) } @@ -167,7 +160,8 @@ private val state = SwapSuccessStateHolder( fromTokenIconState = TokenIconState.Loading, toTokenIconState = TokenIconState.Loading, rate = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), - onSecondaryButtonClick = {}, + onExploreButtonClick = {}, + onStatusButtonClick = {}, ) @Preview(showBackground = true) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 83fb774ca3..60c4a28c7a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -23,7 +23,10 @@ import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* -import com.tangem.feature.swap.models.* +import com.tangem.feature.swap.models.ApproveType +import com.tangem.feature.swap.models.SwapStateHolder +import com.tangem.feature.swap.models.UiActions +import com.tangem.feature.swap.models.toDomainApproveType import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter @@ -402,12 +405,18 @@ internal class SwapViewModel @Inject constructor( fromAmount = dataState.amount?.toBigDecimal() ?: BigDecimal.ZERO, toAmount = dataState.swapDataModel?.toTokenAmount?.value ?: BigDecimal.ZERO, txUrl = url, - onSecondaryBtnClick = { + onExploreClick = { val txHash = it.txAddress if (txHash.isNotEmpty()) { swapRouter.openUrl(url) } }, + onStatusClick = { + val txExternalUrl = it.txExternalUrl + if (!txExternalUrl.isNullOrBlank()) { + swapRouter.openUrl(txExternalUrl) + } + }, ) analyticsEventHandler.send(SwapEvents.SwapInProgressScreen) swapRouter.openScreen(SwapNavScreen.Success) From bfd9e26eba55a2d4832aad322f43cef3da8eb7ba Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Dec 2023 14:31:56 +0300 Subject: [PATCH 104/139] Updated on 2026-08-14 --- .../swap/models/states/ProviderState.kt | 2 +- .../swap/ui/ChooseProviderBottomSheet.kt | 2 +- .../tangem/feature/swap/ui/ProviderItem.kt | 4 +-- .../tangem/feature/swap/ui/StateBuilder.kt | 32 ++++++++++++++++--- .../feature/swap/viewmodels/SwapViewModel.kt | 1 + 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index 2d1d66ef31..63088433c5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -22,7 +22,7 @@ sealed class ProviderState { val name: String, val type: String, val iconUrl: String, - val rate: String, + val subtitle: TextReference, val selectionType: SelectionType, val additionalBadge: AdditionalBadge, val percentLowerThenBest: Float?, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 30954893a7..cf5fa35a88 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -87,7 +87,7 @@ private fun ChooseProviderBottomSheet_Preview() { name = "1inch", type = "DEX", iconUrl = "", - rate = "1 000 000", + subtitle = stringReference("1 000 000"), additionalBadge = ProviderState.AdditionalBadge.BestTrade, percentLowerThenBest = -1.0f, selectionType = ProviderState.SelectionType.SELECT, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 550fbfcad6..79bfe25d5e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -138,7 +138,7 @@ private fun ProviderContentState( ), ) { Text( - text = state.rate, + text = state.subtitle.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, overflow = TextOverflow.Ellipsis, @@ -387,7 +387,7 @@ private fun ProviderItem_Content_Preview() { name = "1inch", type = "DEX", iconUrl = "", - rate = "1 000 000", + subtitle = stringReference("1 000 000"), additionalBadge = ProviderState.AdditionalBadge.PermissionRequired, percentLowerThenBest = -1.0f, selectionType = ProviderState.SelectionType.SELECT, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6a5f0a46f8..49bc2f821b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -206,6 +206,7 @@ internal class StateBuilder( fromToken: CryptoCurrency, swapProvider: SwapProvider, bestRatedProviderId: String, + isManyProviders: Boolean, selectedFeeType: FeeType, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -299,6 +300,7 @@ internal class StateBuilder( isBestRate = bestRatedProviderId == swapProvider.providerId, fromTokenInfo = quoteModel.fromTokenInfo, toTokenInfo = quoteModel.toTokenInfo, + isNeedBadge = isManyProviders, selectionType = ProviderState.SelectionType.CLICK, onProviderClick = actions.onProviderClick, ), @@ -374,7 +376,7 @@ internal class StateBuilder( ): ProviderState { return when (dataError) { is DataError.ExchangeTooSmallAmountError -> { - swapProvider.convertToUnavailableProviderState( + swapProvider.convertToAvailableFromProviderState( alertText = resourceReference( R.string.express_provider_min_amount, wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)), @@ -606,7 +608,7 @@ internal class StateBuilder( showStatusButton = providerState.type == ExchangeProviderType.CEX.name, providerIcon = providerState.iconUrl, fee = TextReference.Str("${fee.amountCrypto} ${fee.symbolCrypto} (${fee.amountFiatFormatted})"), - rate = TextReference.Str(providerState.rate), + rate = providerState.subtitle, fromTokenAmount = TextReference.Str(txState.fromAmount.orEmpty()), toTokenAmount = TextReference.Str(txState.toAmount.orEmpty()), fromTokenFiatAmount = TextReference.Str(fromFiatAmount), @@ -911,11 +913,13 @@ internal class StateBuilder( return "$firstAddressPart...$secondAddressPart" } + @Suppress("LongParameterList") private fun SwapProvider.convertToContentClickableProviderState( isBestRate: Boolean, fromTokenInfo: TokenSwapInfo, toTokenInfo: TokenSwapInfo, selectionType: ProviderState.SelectionType, + isNeedBadge: Boolean, onProviderClick: (String) -> Unit, ): ProviderState { val rate = toTokenInfo.tokenAmount.value.calculateRate( @@ -925,7 +929,7 @@ internal class StateBuilder( val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" - val badge = if (isBestRate) { + val badge = if (isNeedBadge && isBestRate) { ProviderState.AdditionalBadge.BestTrade } else { ProviderState.AdditionalBadge.Empty @@ -935,7 +939,7 @@ internal class StateBuilder( name = this.name, iconUrl = this.imageLarge, type = this.type.toString(), - rate = rateString, + subtitle = stringReference(rateString), additionalBadge = badge, selectionType = selectionType, percentLowerThenBest = null, @@ -964,7 +968,7 @@ internal class StateBuilder( name = this.name, iconUrl = this.imageLarge, type = this.type.toString(), - rate = rateString, + subtitle = stringReference(rateString), additionalBadge = additionalBadge, selectionType = selectionType, percentLowerThenBest = pricesLowerBest[this], @@ -988,6 +992,24 @@ internal class StateBuilder( ) } + private fun SwapProvider.convertToAvailableFromProviderState( + alertText: TextReference, + selectionType: ProviderState.SelectionType, + onProviderClick: (String) -> Unit, + ): ProviderState { + return ProviderState.Content( + id = this.providerId, + name = this.name, + iconUrl = this.imageLarge, + type = this.type.toString(), + selectionType = selectionType, + subtitle = alertText, + additionalBadge = ProviderState.AdditionalBadge.Empty, + percentLowerThenBest = null, + onProviderClick = onProviderClick, + ) + } + private fun CryptoCurrencyStatus.getFormattedAmount(): String { val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 60c4a28c7a..dae161d290 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -294,6 +294,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromToken.currency, swapProvider = provider, bestRatedProviderId = bestRatedProviderId, + isManyProviders = dataState.lastLoadedSwapStates.isNotEmpty(), selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) } From ef30c91181a35c749639e8dcb342f2e0db454b5d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Dec 2023 14:48:04 +0300 Subject: [PATCH 105/139] Updated on 2026-08-14 --- .../core/ui/components/bottomsheets/TangemBottomSheet.kt | 2 +- data/analytics/.gitignore | 2 +- .../kotlin/com/tangem/data/tokens/di/TokensDataModule.kt | 1 + .../com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 6 +++--- .../com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt | 2 +- .../ui/components/exchange/ExchangeStatusBottomSheet.kt | 2 +- .../tokendetails/viewmodels/TokenDetailsViewModel.kt | 1 - 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 9761a6b1dd..7627f60b0e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -33,7 +33,7 @@ inline fun TangemBottomSheet( sheetState = sheetState, containerColor = contentColor, shape = TangemTheme.shapes.bottomSheetLarge, - dragHandle = { TangemBottomSheetDraggableHeader(color) }, + dragHandle = { TangemBottomSheetDraggableHeader(contentColor) }, ) { content(config.content) } diff --git a/data/analytics/.gitignore b/data/analytics/.gitignore index 3a48006eea..796b96d1c4 100644 --- a/data/analytics/.gitignore +++ b/data/analytics/.gitignore @@ -1 +1 @@ -/buld +/build diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 525a53a01e..db088b565c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -2,6 +2,7 @@ package com.tangem.data.tokens.di import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.repository.* +import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 447edd8665..85be41f3bb 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -468,7 +468,7 @@ internal class SwapInteractorImpl @Inject constructor( toDecimals = currencyToGet.currency.decimals, providerId = swapProvider.providerId, rateType = RateType.FLOAT, - toAddress = currencyToGet.value.networkAddress?.defaultAddress ?: "", + toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "", ) val txData = walletManagersFacade.createTransaction( @@ -804,7 +804,7 @@ internal class SwapInteractorImpl @Inject constructor( toDecimals = toToken.currency.decimals, providerId = provider.providerId, rateType = RateType.FLOAT, - toAddress = toToken.value.networkAddress?.defaultAddress ?: "", + toAddress = toToken.value.networkAddress?.defaultAddress?.value ?: "", ).let { val swapData = it.dataModel if (swapData != null) { @@ -911,7 +911,7 @@ internal class SwapInteractorImpl @Inject constructor( getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> val txFeeResult = getFeeUseCase( amount = amount.value, - destination = fromToken.value.networkAddress?.defaultAddress ?: "", + destination = fromToken.value.networkAddress?.defaultAddress?.value ?: "", userWalletId = userWalletId, cryptoCurrency = fromToken.currency, ).firstOrNull() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index cf5fa35a88..2dd33c473f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -25,7 +25,7 @@ import kotlinx.collections.immutable.toImmutableList fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { TangemBottomSheet( config = config, - color = TangemTheme.colors.background.tertiary, + contentColor = TangemTheme.colors.background.tertiary, ) { content: ChooseProviderBottomSheetConfig -> ChooseProviderBottomSheetContent(content = content) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index f02ca3d0a7..1f047b8f60 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -27,7 +27,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTrans internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) { TangemBottomSheet( config = config, - color = TangemTheme.colors.background.tertiary, + contentColor = TangemTheme.colors.background.tertiary, ) { content: ExchangeStatusBottomSheetConfig -> ExchangeStatusBottomSheetContent(content = content) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index b4173d44bf..7d2d6a2019 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -77,7 +77,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, - private val walletManagersFacade: WalletManagersFacade, private val isDemoCardUseCase: IsDemoCardUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, From 7f0c9e40be50ae778f69a3d921a161e31470121e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Dec 2023 16:48:48 +0300 Subject: [PATCH 106/139] Updated on 2026-08-14 --- data/analytics/.gitignore | 2 +- .../walletmanager/utils/UpdateWalletManagerResultFactory.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/data/analytics/.gitignore b/data/analytics/.gitignore index 3a48006eea..796b96d1c4 100644 --- a/data/analytics/.gitignore +++ b/data/analytics/.gitignore @@ -1 +1 @@ -/buld +/build diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt index a26d96acc6..e865fcc408 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -10,7 +10,6 @@ import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import timber.log.Timber import java.math.BigDecimal -import java.util.concurrent.TimeUnit import com.tangem.blockchain.common.address.Address as SdkAddress internal class UpdateWalletManagerResultFactory { From f33ada03528e4c09ea2a38d346c62c70d1239663 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Dec 2023 12:47:48 +0300 Subject: [PATCH 107/139] Updated on 2026-08-14 --- .../DefaultMarketCryptoCurrencyRepository.kt | 2 +- .../feature/swap/domain/SwapInteractor.kt | 2 ++ .../feature/swap/domain/SwapInteractorImpl.kt | 4 ++++ features/swap/presentation/build.gradle.kts | 2 ++ .../feature/swap/presentation/SwapFragment.kt | 5 +++++ .../tangem/feature/swap/router/SwapRouter.kt | 20 +++++++++++++++++++ .../tangem/feature/swap/ui/StateBuilder.kt | 5 ++--- .../feature/swap/viewmodels/SwapViewModel.kt | 6 +++++- 8 files changed, 41 insertions(+), 5 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt index 1e3f9eef51..84188ea775 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -15,7 +15,7 @@ class DefaultMarketCryptoCurrencyRepository( return assetsStore.getSyncOrNull(userWalletId)?.find { it.network == cryptoCurrency.network.backendId && - it.contractAddress == contractAddress + it.contractAddress.equals(contractAddress, ignoreCase = true) }?.exchangeAvailable ?: false } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index c75b56fd63..9c8ebfcbf3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -90,4 +90,6 @@ interface SwapInteractor { initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, ): CryptoCurrencyStatus? + + fun getNativeToken(networkId: String): CryptoCurrency } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 85be41f3bb..23cef2f968 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -596,6 +596,10 @@ internal class SwapInteractorImpl @Inject constructor( ?: state.toGroup.available.firstOrNull()?.currencyStatus } + override fun getNativeToken(networkId: String): CryptoCurrency { + return repository.getNativeTokenForNetwork(networkId) + } + @Deprecated("used in old swap mechanism") private fun getTangemFee(): Double { return repository.getTangemFee() diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index dc0cdcabd2..b2ea5cda48 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) implementation(projects.core.utils) implementation(projects.core.ui) implementation(projects.common) @@ -45,6 +46,7 @@ dependencies { /** Api */ implementation(projects.features.swap.api) + implementation(projects.features.tokendetails.api) /** Domain */ implementation(projects.features.swap.domain) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 00744ef60a..3b16f0df21 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.Crossfade import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels +import com.tangem.core.navigation.ReduxNavController import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment @@ -26,6 +27,9 @@ class SwapFragment : ComposeFragment() { @Inject override lateinit var appThemeModeHolder: AppThemeModeHolder + @Inject + lateinit var reduxNavController: ReduxNavController + private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { @@ -35,6 +39,7 @@ class SwapFragment : ComposeFragment() { SwapRouter( fragmentManager = WeakReference(parentFragmentManager), customTabsManager = CustomTabsManager(WeakReference(context)), + reduxNavController = reduxNavController, ), ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt index 62ce0901c5..081730868d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt @@ -3,12 +3,20 @@ package com.tangem.feature.swap.router import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.core.os.bundleOf import androidx.fragment.app.FragmentManager +import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.ReduxNavController +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import java.lang.ref.WeakReference internal class SwapRouter( private val fragmentManager: WeakReference, private val customTabsManager: CustomTabsManager, + private val reduxNavController: ReduxNavController, ) { var currentScreen by mutableStateOf(SwapNavScreen.Main) @@ -29,6 +37,18 @@ internal class SwapRouter( fun openUrl(url: String) { customTabsManager.openUrl(url) } + + fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { + reduxNavController.navigate( + action = NavigationAction.NavigateTo( + screen = AppScreen.WalletDetails, + bundle = bundleOf( + TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, + TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency, + ), + ), + ) + } } enum class SwapNavScreen { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 49bc2f821b..44ea71aac2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -222,8 +222,7 @@ internal class StateBuilder( ), ) } - if (quoteModel.preparedSwapConfigState.isAllowedToSpend && - !quoteModel.preparedSwapConfigState.isFeeEnough && + if (!quoteModel.preparedSwapConfigState.isFeeEnough && quoteModel.preparedSwapConfigState.isBalanceEnough ) { warnings.add( @@ -896,7 +895,7 @@ internal class StateBuilder( ), iconResId = fromToken.networkIconResId, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.common_buy_currency, wrappedList(fromToken.name)), + text = resourceReference(R.string.common_buy_currency, wrappedList(fromToken.network.currencySymbol)), onClick = onBuyClick, ), ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index dae161d290..65d865ae21 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -748,7 +748,11 @@ internal class SwapViewModel @Inject constructor( ) } }, - onBuyClick = {}, + onBuyClick = { + swapInteractor.getSelectedWallet()?.let { + swapRouter.openTokenDetails(it.walletId, swapInteractor.getNativeToken(dataState.networkId)) + } + }, ) } From de49ae56f7ef6bd062c93d3598a92959714880c4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Dec 2023 11:48:06 +0200 Subject: [PATCH 108/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 7 +++++++ .../domain/models/ui/TokensDataStateExpress.kt | 16 ---------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 23cef2f968..37afca0159 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -79,6 +79,13 @@ internal class SwapInteractorImpl @Inject constructor( it.currency.getContractAddress() != currency.getContractAddress() } + if (walletCurrencyStatusesExceptInitial.isEmpty()) { + return TokensDataStateExpress( + fromGroup = CurrenciesGroup(emptyList(), emptyList()), + toGroup = CurrenciesGroup(emptyList(), emptyList()), + ) + } + val pairsLeast = getPairs( initialCurrency = LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index 752e9184a7..b2c0d0c359 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -1,6 +1,5 @@ package com.tangem.feature.swap.domain.models.ui -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo data class TokensDataStateExpress( @@ -11,19 +10,4 @@ data class TokensDataStateExpress( data class CurrenciesGroup( val available: List, val unavailable: List, -) - -data class FoundTokensStateExpress( - val tokensInWallet: List, - val loadedTokens: List, -) - -data class TokenWithBalanceExpress( - val token: CryptoCurrency, - val tokenBalanceData: TokenBalanceDataExpress? = null, -) - -data class TokenBalanceDataExpress( - val amount: String?, - val amountEquivalent: String?, ) \ No newline at end of file From 3e9e892acae2fbf7cd8c356850f6d4510d2511ac Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Dec 2023 12:58:51 +0300 Subject: [PATCH 109/139] Updated on 2026-08-14 --- .../DefaultWalletManagersFacade.kt | 6 +--- .../viewmodel/MemoVerification.kt | 7 +++- .../presentation/viewmodel/SendViewModel.kt | 34 ++++++++++++++++--- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index df99d53034..d1a57e7726 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -463,11 +463,7 @@ class DefaultWalletManagersFacade( derivationPath = network.derivationPath.value, ) - val txData = walletManager?.createTransaction(amount, fee, destination)?.copy( - extras = null, // todo add memo [[REDACTED_JIRA]] - ) - - return txData + return walletManager?.createTransaction(amount, fee, destination) } override suspend fun sendTransaction( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt index ae24d705fd..2e63cb625f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/MemoVerification.kt @@ -41,7 +41,12 @@ private fun isAssignableValue(value: String): Boolean { } } -private enum class XlmMemoType { TEXT, ID } +fun determineXlmMemoType(value: String): XlmMemoType = when { + value.isNotEmpty() && value.isDigitsOnly() -> XlmMemoType.ID + else -> XlmMemoType.TEXT +} + +enum class XlmMemoType { TEXT, ID } private const val XRP_TAG_MAX_NUMBER = 4294967295 private const val XLM_MEMO_MAX_LENGTH = 28 \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 4fdc321d9f..7d29e9d5cd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -6,8 +6,15 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.* import androidx.paging.PagingData import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras +import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras +import com.tangem.blockchain.blockchains.stellar.StellarMemo +import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras +import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpAddressService +import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionExtras import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee @@ -80,7 +87,7 @@ internal class SendViewModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() - private var inneRrouter: InnerSendRouter by Delegates.notNull() + private var innerRouter: InnerSendRouter by Delegates.notNull() private var stateRouter: StateRouter by Delegates.notNull() private val stateFactory = SendStateFactory( @@ -111,7 +118,7 @@ internal class SendViewModel @Inject constructor( } fun setRouter(router: InnerSendRouter, stateRouter: StateRouter) { - inneRrouter = router + innerRouter = router this.stateRouter = stateRouter uiState = uiState.copy(currentState = stateRouter.currentState) } @@ -428,7 +435,7 @@ internal class SendViewModel @Inject constructor( destination = recipient.value, userWalletId = userWalletId, network = cryptoCurrency.network, - ) ?: return + )?.copy(extras = getMemoExtras(cryptoCurrency.network.id.value, memo?.value)) ?: return sendTransactionUseCase( txData = txData, @@ -475,7 +482,7 @@ internal class SendViewModel @Inject constructor( override fun showFee() = stateRouter.showFee(isFromSend = true) - override fun onExploreClick(txUrl: String) = inneRrouter.openUrl(txUrl) + override fun onExploreClick(txUrl: String) = innerRouter.openUrl(txUrl) private fun getTxUrl(hash: String): String { val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) @@ -491,6 +498,25 @@ internal class SendViewModel @Inject constructor( } // endregion + private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { + val blockchain = Blockchain.fromId(networkId) + if (memo == null) return null + return when (blockchain) { + Blockchain.Stellar -> { + val xmlMemo = when (determineXlmMemoType(memo)) { + XlmMemoType.TEXT -> StellarMemo.Text(memo) + XlmMemoType.ID -> StellarMemo.Id(memo.toBigInteger()) + } + StellarTransactionExtras(xmlMemo) + } + Blockchain.Binance -> BinanceTransactionExtras(memo) + Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) } + Blockchain.Cosmos -> CosmosTransactionExtras(memo) + Blockchain.TON -> TonTransactionExtras(memo) + else -> null + } + } + companion object { private const val XRP_X_ADDRESS = 'X' private const val DEFAULT_VALUE = "0.00" From aa5bbce50ee55029fb7f2fa4f50a8d829f8fcc7b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Dec 2023 16:21:33 +0200 Subject: [PATCH 110/139] Updated on 2026-08-14 --- .../data/tokens/repository/DefaultCurrenciesRepository.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 85e522335d..01531715e0 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -94,12 +94,14 @@ internal class DefaultCurrenciesRepository( val newCurrencies = (newCoins + filteredCurrencies).distinct() + val updatedResponse = savedCurrencies.copy( + tokens = savedCurrencies.tokens + newCurrencies.map(userTokensResponseFactory::createResponseToken), + ) storeAndPushTokens( userWalletId = userWalletId, - response = savedCurrencies.copy( - tokens = savedCurrencies.tokens + newCurrencies.map(userTokensResponseFactory::createResponseToken), - ), + response = updatedResponse ) + fetchExchangeableUserMarketCoinsByIds(userWalletId, updatedResponse) } } From 54f13f10829bc17d876409b9ddd2e3f7d9d06213 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Dec 2023 16:28:06 +0200 Subject: [PATCH 111/139] Updated on 2026-08-14 --- .../tangem/feature/swap/domain/SwapInteractorImpl.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 37afca0159..5d93881660 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.domain +import arrow.core.flatten import arrow.core.getOrElse import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType @@ -129,9 +130,9 @@ internal class SwapInteractorImpl @Inject constructor( } val availableCryptoCurrencies = filteredPairs.mapNotNull { pair -> - val status = findCryptoCurrencyStatusByLeastInfo(tokenInfoForAvailable(pair), cryptoCurrenciesList) - status?.let { CryptoCurrencySwapInfo(it, pair.providers) } - } + val statuses = findCryptoCurrencyStatusByLeastInfo(tokenInfoForAvailable(pair), cryptoCurrenciesList) + statuses.map { CryptoCurrencySwapInfo(it, pair.providers) } + }.flatten() val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies .map { it.currencyStatus } @@ -146,8 +147,8 @@ internal class SwapInteractorImpl @Inject constructor( private fun findCryptoCurrencyStatusByLeastInfo( leastTokenInfo: LeastTokenInfo, cryptoCurrencyStatusesList: List, - ): CryptoCurrencyStatus? { - return cryptoCurrencyStatusesList.find { + ): List { + return cryptoCurrencyStatusesList.filter { it.currency.network.backendId == leastTokenInfo.network && it.currency.getContractAddress() == leastTokenInfo.contractAddress } From 215008cbae80ae2687e539449c9112f90cd940d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Dec 2023 19:41:50 +0500 Subject: [PATCH 112/139] Updated on 2026-08-14 --- .../src/main/assets/contract_methods.json | 20 +++++++++++++++++++ gradle/dependencies.toml | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/domain/legacy/src/main/assets/contract_methods.json b/domain/legacy/src/main/assets/contract_methods.json index e07b98657c..f7bad0ed28 100644 --- a/domain/legacy/src/main/assets/contract_methods.json +++ b/domain/legacy/src/main/assets/contract_methods.json @@ -121,5 +121,25 @@ }, "0xd0e30db0": { "name":"deposit" + }, + "0x9f50973e": { + "info":"swapExactETCForTokens", + "source":"https://etc.blockscout.com/tx/0xc748777532541e51368089c71c93cb0e410bf93c13908bea2d07ecbbb65aa839", + "name":"swap" + }, + "0x3417c606": { + "info":"swapETCForExactTokens", + "source":"https://etc.blockscout.com/tx/0xb2c5a4418112c0b29654642f9c724bf7b2f9056fc6dd2c22209ed89fe982f0af", + "name":"swap" + }, + "0x65f705d3": { + "info":"swapExactTokensForETC", + "source":"https://etc.blockscout.com/tx/0xdd5d372e8d770e436bd44367bbc64b57d02e7339a1f1b849f7a995a57c65f3dd", + "name":"swap" + }, + "0xb4bc722b": { + "info":"swapTokensForExactETC", + "source":"https://etc.blockscout.com/tx/0xd79e03ca6b71529c94d576ff78c55b4371e2ef3ff8f9a47007995e5b9e77f879", + "name":"swap" } } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index d4c8361ff8..b13cd5c2fc 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-408" +tangemBlockchainSdk = "develop-409" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-312" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From d5d03d73f2f96895b201c672ef72aa7f3d9809ba Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Dec 2023 17:49:05 +0300 Subject: [PATCH 113/139] Updated on 2026-08-14 --- .../core/ui/components/rows/ActionRow.kt | 25 ++-- .../repository/DefaultCurrenciesRepository.kt | 2 +- .../feature/swap/domain/SwapInteractor.kt | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 121 +++++++++++++----- .../models/domain/PreparedSwapConfigState.kt | 12 +- .../swap/domain/models/ui/SwapState.kt | 2 + .../tangem/feature/swap/ui/ProviderItem.kt | 87 +++++++------ .../tangem/feature/swap/ui/StateBuilder.kt | 119 +++++++++++------ .../feature/swap/ui/SwapScreenContent.kt | 4 +- .../tangem/feature/swap/ui/TransactionCard.kt | 47 ++++--- .../feature/swap/viewmodels/SwapViewModel.kt | 9 +- .../tangem/lib/crypto/models/ProxyAmount.kt | 9 +- 12 files changed, 289 insertions(+), 149 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt index 86817c690a..0c2a785acc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.rows +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Icon @@ -32,16 +33,20 @@ fun SimpleActionRow(title: String, description: String, modifier: Modifier = Mod .align(Alignment.CenterStart), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { - Text( - text = title, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - ) - Text( - text = description, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) + AnimatedContent(targetState = title, label = "") { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + } + AnimatedContent(targetState = description, label = "") { + Text( + text = it, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } } if (isClickable) { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 01531715e0..06e956fa49 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -99,7 +99,7 @@ internal class DefaultCurrenciesRepository( ) storeAndPushTokens( userWalletId = userWalletId, - response = updatedResponse + response = updatedResponse, ) fetchExchangeableUserMarketCoinsByIds(userWalletId, updatedResponse) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 9c8ebfcbf3..d36610f673 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -64,6 +64,7 @@ interface SwapInteractor { currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, amountToSwap: String, + includeFeeInAmount: IncludeFeeInAmount, fee: TxFee, ): TxState diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 5d93881660..0e3c786b0a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -294,7 +294,7 @@ internal class SwapInteractorImpl @Inject constructor( transactionManager.updateWalletManager(networkId, derivationPath) } return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { - provider to loadSwapData( + provider to loadDexSwapData( provider = provider, networkId = networkId, fromToken = fromToken, @@ -312,8 +312,8 @@ internal class SwapInteractorImpl @Inject constructor( networkId = networkId, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - providerType = provider.type, - selectedFee = selectedFee, + txFee = TxFeeState.Empty, + includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex ) } } @@ -327,7 +327,7 @@ internal class SwapInteractorImpl @Inject constructor( isBalanceWithoutFeeEnough: Boolean, selectedFee: FeeType, ): Pair { - return provider to loadQuoteData( + return provider to loadCexQuoteData( exchangeProviderType = ExchangeProviderType.CEX, networkId = networkId, amount = amount, @@ -340,7 +340,6 @@ internal class SwapInteractorImpl @Inject constructor( ) } - @Deprecated("used in old swap mechanism") override suspend fun onSwap( swapProvider: SwapProvider, networkId: String, @@ -348,17 +347,22 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, amountToSwap: String, + includeFeeInAmount: IncludeFeeInAmount, fee: TxFee, ): TxState { return when (swapProvider.type) { ExchangeProviderType.CEX -> { val amountDecimal = toBigDecimalOrNull(amountToSwap) val amount = SwapAmount(requireNotNull(amountDecimal), getTokenDecimals(currencyToSend.currency)) - + val amountToSwapWithFee = if (includeFeeInAmount is IncludeFeeInAmount.Included) { + includeFeeInAmount.amountSubtractFee + } else { + amount + } onSwapCex( currencyToSend = currencyToSend, currencyToGet = currencyToGet, - amount = amount, + amount = amountToSwapWithFee, txFee = fee, swapProvider = swapProvider, userWalletId = requireNotNull(getSelectedWallet()).walletId, @@ -389,20 +393,19 @@ internal class SwapInteractorImpl @Inject constructor( return state } val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) - val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = state.txFee) - val isBalanceIncludeFeeEnough = - isBalanceEnough(fromToken, amount, feeByPriority) - val isFeeEnough = checkFeeIsEnough( - fee = feeByPriority, - spendAmount = amount, + val includeFeeInAmount = getIncludeFeeInAmount( networkId = networkId, + txFee = state.txFee, + amount = amount, fromToken = fromToken.currency, + selectedFee = selectedFee, ) return state.copy( permissionState = PermissionDataState.Empty, preparedSwapConfigState = state.preparedSwapConfigState.copy( - isBalanceEnough = isBalanceIncludeFeeEnough, - isFeeEnough = isFeeEnough, + isBalanceEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, + isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, + includeFeeInAmount = includeFeeInAmount, ), ) } @@ -666,7 +669,7 @@ internal class SwapInteractorImpl @Inject constructor( * Load quote data calls only if spend is not allowed for token contract address */ @Suppress("LongParameterList") - private suspend fun loadQuoteData( + private suspend fun loadCexQuoteData( exchangeProviderType: ExchangeProviderType, networkId: String, amount: SwapAmount, @@ -680,12 +683,31 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency return coroutineScope { + val txFee = if (provider.type == ExchangeProviderType.CEX) { + getFeeForCex(amount, fromTokenStatus, networkId) + } else { + TxFeeState.Empty + } + + val includeFeeInAmount = getIncludeFeeInAmount( + networkId = networkId, + txFee = txFee, + amount = amount, + fromToken = fromToken, + selectedFee = selectedFee, + ) + val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) { + includeFeeInAmount.amountSubtractFee + } else { + amount + } + val quotes = repository.findBestQuote( fromContractAddress = fromToken.getContractAddress(), fromNetwork = fromToken.network.backendId, toContractAddress = toToken.getContractAddress(), toNetwork = toToken.network.backendId, - fromAmount = amount.toStringWithRightOffset(), + fromAmount = amountToRequest.toStringWithRightOffset(), fromDecimals = amount.decimals, providerId = provider.providerId, toDecimals = toToken.decimals, @@ -701,8 +723,8 @@ internal class SwapInteractorImpl @Inject constructor( networkId = networkId, isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - providerType = provider.type, - selectedFee = selectedFee, + txFee = txFee, + includeFeeInAmount = includeFeeInAmount, ) } } @@ -716,16 +738,11 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, - providerType: ExchangeProviderType, - selectedFee: FeeType, + txFee: TxFeeState, + includeFeeInAmount: IncludeFeeInAmount, ): SwapState { val quoteModel = quoteDataModel.dataModel if (quoteModel != null) { - val txFee = if (providerType == ExchangeProviderType.CEX) { - getFeeForCex(amount, fromToken, networkId) - } else { - TxFeeState.Empty - } val swapState = updateBalances( networkId = networkId, fromTokenStatus = fromToken, @@ -754,19 +771,13 @@ internal class SwapInteractorImpl @Inject constructor( ) } ExchangeProviderType.CEX -> { - val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFee) - val isFeeEnough = checkFeeIsEnough( - fee = feeByPriority, - spendAmount = amount, - networkId = networkId, - fromToken = fromToken.currency, - ) swapState.copy( permissionState = PermissionDataState.Empty, preparedSwapConfigState = PreparedSwapConfigState( - isFeeEnough = isFeeEnough, + isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, isAllowedToSpend = isAllowedToSpend, isBalanceEnough = isBalanceWithoutFeeEnough, + includeFeeInAmount = includeFeeInAmount, ), ) } @@ -783,6 +794,45 @@ internal class SwapInteractorImpl @Inject constructor( } } + private suspend fun getIncludeFeeInAmount( + networkId: String, + txFee: TxFeeState, + amount: SwapAmount, + fromToken: CryptoCurrency, + selectedFee: FeeType, + ): IncludeFeeInAmount { + if (fromToken is CryptoCurrency.Token) { + return IncludeFeeInAmount.Excluded + } + val feeValue = when (txFee) { + TxFeeState.Empty -> BigDecimal.ZERO + is TxFeeState.MultipleFeeState -> if (selectedFee == FeeType.NORMAL) { + txFee.normalFee.feeValue + } else { + txFee.priorityFee.feeValue + } + is TxFeeState.SingleFeeState -> txFee.fee.feeValue + } + + val tokenForFeeBalance = + userWalletManager.getNativeTokenBalance(networkId, derivationPath) ?: ProxyAmount.empty() + val amountWithFee = amount.value + feeValue + return if (amountWithFee < tokenForFeeBalance.value) { + IncludeFeeInAmount.Excluded + } else { + if (feeValue < amount.value) { + IncludeFeeInAmount.Included( + SwapAmount( + tokenForFeeBalance.value - feeValue, + transactionManager.getNativeTokenDecimals(networkId), + ), + ) + } else { + IncludeFeeInAmount.BalanceNotEnough + } + } + } + private suspend fun getFormattedFiatFees(networkId: String, vararg fees: BigDecimal): List { val appCurrency = userWalletManager.getUserAppCurrency() val nativeToken = repository.getNativeTokenForNetwork(networkId) @@ -798,7 +848,7 @@ internal class SwapInteractorImpl @Inject constructor( * Load swap data calls only if spend is allowed for token contract address */ @Suppress("LongParameterList") - private suspend fun loadSwapData( + private suspend fun loadDexSwapData( provider: SwapProvider, networkId: String, fromToken: CryptoCurrencyStatus, @@ -856,6 +906,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = true, isBalanceEnough = isBalanceIncludeFeeEnough, isFeeEnough = isFeeEnough, + includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex ), ) } else { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt index 8b9f1c963b..53cfae4ded 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt @@ -1,5 +1,7 @@ package com.tangem.feature.swap.domain.models.domain +import com.tangem.feature.swap.domain.models.SwapAmount + /** * Prepared swap config state that contains flags to determine * @@ -7,8 +9,16 @@ package com.tangem.feature.swap.domain.models.domain * @property isBalanceEnough shows is balance of token enough * @property isFeeEnough shows is amount of main coin enough for fee */ +// todo Refactor this state data class PreparedSwapConfigState( val isAllowedToSpend: Boolean, val isBalanceEnough: Boolean, val isFeeEnough: Boolean, -) \ No newline at end of file + val includeFeeInAmount: IncludeFeeInAmount, +) + +sealed class IncludeFeeInAmount { + data class Included(val amountSubtractFee: SwapAmount) : IncludeFeeInAmount() + object Excluded : IncludeFeeInAmount() + object BalanceNotEnough : IncludeFeeInAmount() +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 93a5795340..d031891ba3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState import com.tangem.feature.swap.domain.models.domain.SwapDataModel import java.math.BigDecimal @@ -18,6 +19,7 @@ sealed interface SwapState { isAllowedToSpend = false, isBalanceEnough = false, isFeeEnough = false, + includeFeeInAmount = IncludeFeeInAmount.Excluded, ), val permissionState: PermissionDataState = PermissionDataState.Empty, val swapDataModel: SwapDataModel? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 79bfe25d5e..3dead8dfcd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.CircularProgressIndicator @@ -110,17 +111,21 @@ private fun ProviderContentState( modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), ) { Row { - Text( - text = state.name, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.type, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), - ) + AnimatedContent(targetState = state.name, label = "") { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, + ) + } + AnimatedContent(targetState = state.type, label = "") { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + } when (state.additionalBadge) { ProviderState.AdditionalBadge.BestTrade -> BestTradeItem(Modifier.padding(start = TangemTheme.dimens.spacing4)) @@ -137,23 +142,27 @@ private fun ProviderContentState( end = TangemTheme.dimens.spacing56, ), ) { - Text( - text = state.subtitle.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - if (state.percentLowerThenBest != null) { + AnimatedContent(targetState = state.subtitle, label = "") { Text( - text = "-${state.percentLowerThenBest}%", + text = it.resolveReference(), style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.warning, - modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + color = TangemTheme.colors.text.tertiary, overflow = TextOverflow.Ellipsis, maxLines = 1, ) } + if (state.percentLowerThenBest != null) { + AnimatedContent(targetState = state.percentLowerThenBest, label = "") { + Text( + text = "-$it%", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.warning, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + } } } } @@ -198,24 +207,30 @@ private fun ProviderUnavailableState( modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), ) { Row { + AnimatedContent(targetState = state.name, label = "") { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + AnimatedContent(targetState = state.type, label = "") { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + } + } + AnimatedContent(targetState = state.alertText, label = "") { Text( - text = state.name, - style = TangemTheme.typography.caption2, + text = it.resolveReference(), + style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, - ) - Text( - text = state.type, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) } - Text( - text = state.alertText.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), - ) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 44ea71aac2..6d540ed412 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -14,9 +14,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType -import com.tangem.feature.swap.domain.models.domain.NetworkInfo -import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* @@ -211,41 +209,7 @@ internal class StateBuilder( ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val warnings = mutableListOf() - if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && - quoteModel.preparedSwapConfigState.isFeeEnough && - quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest - ) { - warnings.add( - SwapWarning.PermissionNeeded( - createPermissionNotificationConfig(fromToken.symbol), - ), - ) - } - if (!quoteModel.preparedSwapConfigState.isFeeEnough && - quoteModel.preparedSwapConfigState.isBalanceEnough - ) { - warnings.add( - SwapWarning.UnableToCoverFeeWarning( - createUnableToCoverFeeNotificationConfig( - fromToken = fromToken, - onBuyClick = actions.onBuyClick, - ), - ), - ) - } - if (!quoteModel.preparedSwapConfigState.isBalanceEnough) { - warnings.add(SwapWarning.InsufficientFunds) - } - - if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) { - warnings.add( - SwapWarning.HighPriceImpact( - priceImpact = (quoteModel.priceImpact * HUNDRED_PERCENTS).toInt(), - notificationConfig = highPriceImpactNotificationConfig(), - ), - ) - } + val warnings = getWarningsForSuccessState(quoteModel, fromToken) val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus @@ -288,9 +252,7 @@ internal class StateBuilder( ), fee = feeState, swapButton = SwapButton( - enabled = quoteModel.preparedSwapConfigState.isAllowedToSpend && - quoteModel.preparedSwapConfigState.isBalanceEnough && - quoteModel.preparedSwapConfigState.isFeeEnough, + enabled = getSwapButtonEnabled(quoteModel.preparedSwapConfigState), loading = false, onClick = actions.onSwapClick, ), @@ -306,6 +268,71 @@ internal class StateBuilder( ) } + private fun getWarningsForSuccessState( + quoteModel: SwapState.QuotesLoadedState, + fromToken: CryptoCurrency, + ): List { + val warnings = mutableListOf() + if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && + quoteModel.preparedSwapConfigState.isFeeEnough && + quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest + ) { + warnings.add( + SwapWarning.PermissionNeeded( + createPermissionNotificationConfig(fromToken.symbol), + ), + ) + } + when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { + is IncludeFeeInAmount.Included -> + warnings.add( + SwapWarning.GeneralWarning( + createNetworkFeeCoverageNotificationConfig(), + ), + ) + else -> Unit + } + if (!quoteModel.preparedSwapConfigState.isFeeEnough && + quoteModel.preparedSwapConfigState.isBalanceEnough + ) { + warnings.add( + SwapWarning.UnableToCoverFeeWarning( + createUnableToCoverFeeNotificationConfig( + fromToken = fromToken, + onBuyClick = actions.onBuyClick, + ), + ), + ) + } + // check isBalanceEnough, but for dex includeFeeInAmount always Excluded + if (!quoteModel.preparedSwapConfigState.isBalanceEnough && + quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included + ) { + warnings.add(SwapWarning.InsufficientFunds) + } + + if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) { + warnings.add( + SwapWarning.HighPriceImpact( + priceImpact = (quoteModel.priceImpact * HUNDRED_PERCENTS).toInt(), + notificationConfig = highPriceImpactNotificationConfig(), + ), + ) + } + return warnings + } + + private fun getSwapButtonEnabled(preparedSwapConfigState: PreparedSwapConfigState): Boolean { + return when (preparedSwapConfigState.includeFeeInAmount) { + IncludeFeeInAmount.BalanceNotEnough -> false + IncludeFeeInAmount.Excluded -> + preparedSwapConfigState.isAllowedToSpend && + preparedSwapConfigState.isBalanceEnough && + preparedSwapConfigState.isFeeEnough + is IncludeFeeInAmount.Included -> true + } + } + fun createQuotesErrorState( uiStateHolder: SwapStateHolder, swapProvider: SwapProvider, @@ -406,7 +433,7 @@ internal class StateBuilder( notificationConfig = NotificationConfig( title = resourceReference(R.string.common_error), subtitle = resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())), - iconResId = R.drawable.ic_alert_circle_24, + iconResId = R.drawable.img_attention_20, ), ) } @@ -900,6 +927,14 @@ internal class StateBuilder( ), ) } + + private fun createNetworkFeeCoverageNotificationConfig(): NotificationConfig { + return NotificationConfig( + title = resourceReference(R.string.send_network_fee_warning_title), + subtitle = resourceReference(R.string.send_network_fee_warning_content), + iconResId = R.drawable.img_attention_20, + ) + } // end region private fun getShortAddressValue(fullAddress: String): String { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 0ba2059fab..949ead0f64 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -74,7 +74,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings) - if (state.permissionState is SwapPermissionState.InProgress) { + AnimatedVisibility(visible = state.permissionState is SwapPermissionState.InProgress) { CardWithIcon( title = stringResource(id = R.string.swapping_pending_transaction_title), description = stringResource(id = R.string.swapping_pending_transaction_subtitle), @@ -236,6 +236,7 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { } } +@Suppress("LongMethod") @Composable private fun SwapWarnings(warnings: List) { Column( @@ -289,7 +290,6 @@ private fun SwapWarnings(warnings: List) { is SwapWarning.GeneralWarning -> { Notification( config = warning.notificationConfig, - iconTint = TangemTheme.colors.icon.warning, ) } else -> {} diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index f4bd0ae799..258e8f5ddf 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.ui import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -194,14 +195,16 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie ) SpacerW16() if (balance.isNotBlank()) { - Text( - text = balance, - color = TangemTheme.colors.text.tertiary, - style = MaterialTheme.typography.body2, - modifier = Modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size20) - .padding(top = TangemTheme.dimens.spacing2), - ) + AnimatedContent(targetState = balance, label = "") { + Text( + text = it, + color = TangemTheme.colors.text.tertiary, + style = MaterialTheme.typography.body2, + modifier = Modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size20) + .padding(top = TangemTheme.dimens.spacing2), + ) + } } else { RectangleShimmer( modifier = Modifier @@ -259,12 +262,14 @@ private fun Content( } } is TransactionCardType.SendCard -> { - AutoSizeTextField( - modifier = sumTextModifier, - textFieldValue = textFieldValue ?: TextFieldValue(), - onAmountChange = { type.onAmountChanged(it) }, - onFocusChange = type.onFocusChanged, - ) + AnimatedContent(targetState = textFieldValue, label = "") { + AutoSizeTextField( + modifier = sumTextModifier, + textFieldValue = it ?: TextFieldValue(), + onAmountChange = { type.onAmountChanged(it) }, + onFocusChange = type.onFocusChanged, + ) + } } } @@ -292,12 +297,14 @@ private fun Content( ) } } else { - Text( - text = amountEquivalent, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), - ) + AnimatedContent(targetState = amountEquivalent, label = "") { + Text( + text = it, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), + ) + } } } else { RectangleShimmer( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 65d865ae21..64afcb59a5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -381,15 +381,22 @@ internal class SwapViewModel @Inject constructor( private fun onSwapClick() { singleTaskScheduler.cancelTask() uiState = stateBuilder.createSwapInProgressState(uiState) + val provider = requireNotNull(dataState.selectedProvider) { "Selected provider is null" } + val lastLoadedQuotesState = dataState.lastLoadedSwapStates[provider] as? SwapState.QuotesLoadedState + if (lastLoadedQuotesState == null) { + Timber.e("Last loaded quotes state is null") + return + } viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { swapInteractor.onSwap( - swapProvider = requireNotNull(dataState.selectedProvider), + swapProvider = provider, networkId = dataState.networkId, swapData = dataState.swapDataModel, currencyToSend = requireNotNull(dataState.fromCryptoCurrency), currencyToGet = requireNotNull(dataState.toCryptoCurrency), amountToSwap = requireNotNull(dataState.amount), + includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount, fee = requireNotNull(dataState.selectedFee), ) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyAmount.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyAmount.kt index aa4c58b0f3..4a877ccca3 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyAmount.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyAmount.kt @@ -13,4 +13,11 @@ data class ProxyAmount( val currencySymbol: String, var value: BigDecimal, val decimals: Int, -) \ No newline at end of file +) { + + companion object { + fun empty(): ProxyAmount { + return ProxyAmount("", BigDecimal.ZERO, 0) + } + } +} \ No newline at end of file From 1bd432de873a9452e02d0f37a6ca661824c0d140 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Dec 2023 18:25:58 +0300 Subject: [PATCH 114/139] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../components/notifications/Notification.kt | 23 ++++-- .../state/SwapTransactionsState.kt | 1 + ...enDetailsSwapTransactionsStateConverter.kt | 70 +++++++++++----- .../exchange/ExchangeStatusBlock.kt | 79 +++++++++---------- .../exchange/ExchangeStatusBottomSheet.kt | 3 + .../exchange/ExchangeStatusItems.kt | 2 +- .../viewmodels/ExchangeStatusFactory.kt | 41 +++------- .../viewmodels/TokenDetailsViewModel.kt | 1 + 9 files changed, 124 insertions(+), 97 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 80dffcfc34..345f06a0f6 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -212,6 +212,7 @@ Deposit received Awaiting deposit Awaiting deposit… + Refunded Sending to you Sending to you… Sent diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 2f30c95eba..c352cf41eb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -8,7 +8,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -42,8 +44,18 @@ import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsSta * >Figma component */ @Composable -fun Notification(config: NotificationConfig, modifier: Modifier = Modifier, iconTint: Color? = null) { - BaseContainer(buttonsState = config.buttonsState, onClick = config.onClick, modifier = modifier) { +fun Notification( + config: NotificationConfig, + modifier: Modifier = Modifier, + containerColor: Color? = null, + iconTint: Color? = null, +) { + BaseContainer( + buttonsState = config.buttonsState, + onClick = config.onClick, + modifier = modifier, + containerColor = containerColor, + ) { Column( modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), @@ -71,9 +83,10 @@ private fun BaseContainer( buttonsState: NotificationConfig.ButtonsState?, onClick: (() -> Unit)?, modifier: Modifier = Modifier, + containerColor: Color? = null, content: @Composable BoxScope.() -> Unit, ) { - val containerColor by rememberUpdatedState( + val tempContainerColor by rememberUpdatedState( newValue = if (buttonsState != null || onClick != null) { TangemTheme.colors.background.primary } else { @@ -88,7 +101,7 @@ private fun BaseContainer( .fillMaxWidth(), enabled = onClick != null, shape = TangemTheme.shapes.roundedCornersXMedium, - color = containerColor, + color = containerColor ?: tempContainerColor, ) { Box(content = content) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index 353bc30d6d..11d4b21719 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -15,6 +15,7 @@ internal data class SwapTransactionsState( val timestamp: TextReference, val fiatSymbol: String, val activeStatus: MutableStateFlow, + val hasFailed: MutableStateFlow, val statuses: MutableStateFlow>, val notification: MutableStateFlow = MutableStateFlow(null), val toCryptoCurrencyId: CryptoCurrency.ID, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 39d60d2436..1c53790afa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -12,6 +12,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState @@ -70,6 +71,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"), fiatSymbol = appCurrency.symbol, statuses = MutableStateFlow(getStatuses(transaction.status?.status)), + hasFailed = MutableStateFlow(transaction.status?.status == ExchangeStatus.Failed), activeStatus = MutableStateFlow(transaction.status?.status), notification = MutableStateFlow( getNotification( @@ -107,11 +109,13 @@ internal class TokenDetailsSwapTransactionsStateConverter( return result.toPersistentList() } - fun updateTxStatus(tx: SwapTransactionsState, status: ExchangeStatus?) { - if (tx.activeStatus.value == status) return - tx.activeStatus.update { status } - tx.notification.update { getNotification(status, tx.txUrl) } - tx.statuses.update { getStatuses(status) } + fun updateTxStatus(tx: SwapTransactionsState, statusModel: ExchangeStatusModel?) { + if (statusModel == null || tx.activeStatus.value == statusModel.status) return + val hasFailed = tx.hasFailed.value || statusModel.status == ExchangeStatus.Failed + tx.activeStatus.update { statusModel.status } + tx.hasFailed.update { hasFailed } + tx.notification.update { getNotification(statusModel.status, statusModel.txUrl) } + tx.statuses.update { getStatuses(statusModel.status, hasFailed) } } private fun getFiatAmount(toFiatAmount: BigDecimal): String { @@ -151,13 +155,15 @@ internal class TokenDetailsSwapTransactionsStateConverter( } } - private fun getStatuses(status: ExchangeStatus?): List { + private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): List { + if (status == null) return emptyList() val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting val isConfirming = status == ExchangeStatus.Confirming val isVerifying = status == ExchangeStatus.Verifying val isExchanging = status == ExchangeStatus.Exchanging val isFailed = status == ExchangeStatus.Failed val isSending = status == ExchangeStatus.Sending + val isRefunded = status == ExchangeStatus.Refunded val isWaitingDone = !isWaiting val isConfirmingDone = !isConfirming && isWaitingDone @@ -167,8 +173,20 @@ internal class TokenDetailsSwapTransactionsStateConverter( return listOf( waitStep(isWaiting, isWaitingDone), confirmStep(isConfirming, isConfirmingDone), - exchangeStep(isExchanging, isExchangingDone, isVerifying, isFailed), - sendStep(isSending, isSendingDone), + exchangeStep( + isExchanging = isExchanging, + isExchangingDone = isExchangingDone, + isRefunded = isRefunded, + hasFailed = hasFailed, + isVerifying = isVerifying, + isFailed = isFailed, + ), + sendStep( + isSending = isSending, + isSendingDone = isSendingDone, + isRefunded = isRefunded, + hasFailed = hasFailed, + ), ) } @@ -196,6 +214,8 @@ internal class TokenDetailsSwapTransactionsStateConverter( private fun exchangeStep( isExchanging: Boolean, isExchangingDone: Boolean, + isRefunded: Boolean, + hasFailed: Boolean, isVerifying: Boolean = false, isFailed: Boolean = false, ) = when { @@ -205,11 +225,11 @@ internal class TokenDetailsSwapTransactionsStateConverter( isActive = true, isDone = false, ) - isFailed -> ExchangeStatusState( + hasFailed || isFailed || isRefunded -> ExchangeStatusState( status = ExchangeStatus.Failed, text = TextReference.Res(R.string.express_exchange_status_failed), - isActive = true, - isDone = false, + isActive = isFailed || isRefunded, + isDone = isRefunded, ) else -> ExchangeStatusState( status = ExchangeStatus.Exchanging, @@ -223,14 +243,22 @@ internal class TokenDetailsSwapTransactionsStateConverter( ) } - private fun sendStep(isSending: Boolean, isSendingDone: Boolean) = ExchangeStatusState( - status = ExchangeStatus.Sending, - text = when { - isSending -> TextReference.Res(R.string.express_exchange_status_sending_active) - isSendingDone -> TextReference.Res(R.string.express_exchange_status_sent) - else -> TextReference.Res(R.string.express_exchange_status_sending) - }, - isActive = isSending, - isDone = isSendingDone, - ) + private fun sendStep(isSending: Boolean, isSendingDone: Boolean, isRefunded: Boolean, hasFailed: Boolean) = when { + hasFailed || isRefunded -> ExchangeStatusState( + status = ExchangeStatus.Refunded, + text = TextReference.Res(R.string.express_exchange_status_refunded), + isActive = false, + isDone = isRefunded, + ) + else -> ExchangeStatusState( + status = ExchangeStatus.Sending, + text = when { + isSending -> TextReference.Res(R.string.express_exchange_status_sending_active) + isSendingDone -> TextReference.Res(R.string.express_exchange_status_sent) + else -> TextReference.Res(R.string.express_exchange_status_sending) + }, + isActive = isSending, + isDone = isSendingDone, + ) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index 909849ae07..9d44f11a9e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components. import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @Composable internal fun ExchangeStatusBlock( statuses: MutableStateFlow>, + showLink: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -48,28 +50,30 @@ internal fun ExchangeStatusBlock( .padding(bottom = TangemTheme.dimens.spacing16), ) { Text( - text = stringResource(id = R.string.common_balance_title), + text = stringResource(id = R.string.express_exchange_status_title), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) SpacerWMax() - Row( - modifier = Modifier.clickable { onClick() }, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_arrow_top_right_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - modifier = Modifier - .size(TangemTheme.dimens.spacing16) - .padding(end = TangemTheme.dimens.spacing2), - ) - Text( - text = stringResource(id = R.string.express_go_to_provider), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) + AnimatedVisibility(visible = showLink) { + Row( + modifier = Modifier.clickable { onClick() }, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_top_right_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(TangemTheme.dimens.spacing16) + .padding(end = TangemTheme.dimens.spacing2), + ) + Text( + text = stringResource(id = R.string.express_go_to_provider), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } } } @@ -99,15 +103,21 @@ private fun ExchangeStatusStep( .size(TangemTheme.dimens.size20), ) { when { - it.status == ExchangeStatus.Failed && !it.isDone -> ExchangeStepWaringOrError( + it.status == ExchangeStatus.Failed -> ExchangeStep( iconRes = R.drawable.ic_close_24, color = TangemTheme.colors.icon.warning, + isDone = it.isDone, ) - it.status == ExchangeStatus.Verifying && !it.isDone -> ExchangeStepWaringOrError( + it.status == ExchangeStatus.Verifying -> ExchangeStep( iconRes = R.drawable.ic_exclamation_24, color = TangemTheme.colors.icon.attention, + isDone = it.isDone, + ) + it.isDone -> ExchangeStep( + iconRes = R.drawable.ic_check_24, + color = TangemTheme.colors.icon.primary1, + isDone = true, ) - it.isDone -> ExchangeStepSuccess() it.isActive -> ExchangeStepInProgress() else -> ExchangeStepDefault() } @@ -153,31 +163,20 @@ private fun ExchangeStepDefault() { } @Composable -private fun ExchangeStepSuccess() { - Icon( - painter = painterResource(id = R.drawable.ic_check_24), - contentDescription = null, - tint = TangemTheme.colors.icon.primary1, - modifier = Modifier - .border( - width = TangemTheme.dimens.size1_5, - color = TangemTheme.colors.field.focused, - shape = CircleShape, - ) - .padding(TangemTheme.dimens.spacing2), - ) -} - -@Composable -private fun ExchangeStepWaringOrError(color: Color, @DrawableRes iconRes: Int) { +private fun ExchangeStep(color: Color, @DrawableRes iconRes: Int, isDone: Boolean) { + val (iconColor, borderColor) = if (isDone) { + TangemTheme.colors.icon.primary1 to TangemTheme.colors.field.focused + } else { + color to color + } Icon( painter = painterResource(id = iconRes), contentDescription = null, - tint = color, + tint = iconColor, modifier = Modifier .border( width = TangemTheme.dimens.size1_5, - color = color, + color = borderColor, shape = CircleShape, ) .padding(TangemTheme.dimens.spacing2), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index 1f047b8f60..743b214df8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -1,6 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -77,6 +78,7 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC SpacerH12() ExchangeStatusBlock( statuses = config.statuses, + showLink = notification.value == null, onClick = config.onGoToProviderClick, ) AnimatedContent( @@ -92,6 +94,7 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC Notification( config = it.config, iconTint = tint, + containerColor = TangemTheme.colors.background.action, modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index 79c9ab8392..c73e3e3051 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -82,7 +82,7 @@ private fun ExchangeStatusItem( modifier = modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) + .background(TangemTheme.colors.background.primary) .clickable { onClick() } .padding(TangemTheme.dimens.spacing12), ) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index b1441a921a..4e7fc63875 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -54,9 +54,9 @@ internal class ExchangeStatusFactory( flow = swapTransactionRepository.getTransactions(userWalletId, cryptoCurrency.id), flow2 = getWalletCryptoCurrencies().conflate(), ) { savedTransactions, cryptoCurrenciesStatusList -> - innerLoadSwapState( - savedTransactions, - cryptoCurrenciesStatusList, + getExchangeStatusState( + savedTransactions = savedTransactions, + cryptoCurrencyStatusList = cryptoCurrenciesStatusList, ) } @@ -76,9 +76,9 @@ internal class ExchangeStatusFactory( suspend fun updateSwapTxStatuses(swapTxList: PersistentList) = withContext(dispatchers.io) { swapTxList.map { tx -> async { - val status = getExchangeStatus(tx.txId)?.status - swapTransactionsStateConverter.updateTxStatus(tx, status) - tx.removeIfFinished(status) + val statusModel = getExchangeStatus(tx.txId) + swapTransactionsStateConverter.updateTxStatus(tx, statusModel) + tx.removeIfFinished(statusModel?.status) } } .awaitAll() @@ -86,27 +86,6 @@ internal class ExchangeStatusFactory( .toPersistentList() } - private suspend fun innerLoadSwapState( - savedTransactions: List?, - cryptoCurrenciesStatusList: List, - ): PersistentList { - val txWithStatuses = savedTransactions?.map { currencySwaps -> - currencySwaps.copy( - transactions = currencySwaps.transactions.map { tx -> - val status = getExchangeStatus(tx.txId) - tx.copy( - status = status, - ) - }, - ) - } - - return getExchangeStatusState( - savedTransactions = txWithStatuses, - cryptoCurrencyStatusList = cryptoCurrenciesStatusList, - ) - } - private suspend fun getExchangeStatus(txId: String): ExchangeStatusModel? { return swapRepository.getExchangeStatus(txId) .fold( @@ -129,8 +108,9 @@ internal class ExchangeStatusFactory( ) } - private suspend fun SwapTransactionsState.removeIfFinished(status: ExchangeStatus?): SwapTransactionsState? { - return if (status == ExchangeStatus.Refunded || status == ExchangeStatus.Finished) { + private suspend fun SwapTransactionsState.removeIfFinished(status: ExchangeStatus?) = when (status) { + null -> null // not found + ExchangeStatus.Refunded, ExchangeStatus.Finished -> { swapTransactionRepository.removeTransaction( userWalletId = userWalletId, fromCryptoCurrencyId = fromCryptoCurrencyId, @@ -138,7 +118,8 @@ internal class ExchangeStatusFactory( txId = txId, ) null - } else { + } + else -> { this } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index ccd48ffb88..62b23a3763 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -524,6 +524,7 @@ internal class TokenDetailsViewModel @Inject constructor( refresh = true, showItemsLoading = uiState.txHistoryState !is TxHistoryState.Content, ) + subscribeOnExchangeTransactionsUpdates() }, ).awaitAll() uiState = stateFactory.getRefreshedState() From f14216de94784e9cba328beda42855454ad7ca57 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 15:28:07 +0300 Subject: [PATCH 115/139] Updated on 2026-08-14 --- .../com/tangem/feature/swap/ui/TransactionCard.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 258e8f5ddf..18d89a4d06 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -262,14 +262,12 @@ private fun Content( } } is TransactionCardType.SendCard -> { - AnimatedContent(targetState = textFieldValue, label = "") { - AutoSizeTextField( - modifier = sumTextModifier, - textFieldValue = it ?: TextFieldValue(), - onAmountChange = { type.onAmountChanged(it) }, - onFocusChange = type.onFocusChanged, - ) - } + AutoSizeTextField( + modifier = sumTextModifier, + textFieldValue = textFieldValue ?: TextFieldValue(), + onAmountChange = { type.onAmountChanged(it) }, + onFocusChange = type.onFocusChanged, + ) } } From 8424eb2464721dcdbb2f0ed895e8c61b42f0c26e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 15:29:11 +0300 Subject: [PATCH 116/139] Updated on 2026-08-14 --- .../presentation/tokendetails/state/SwapTransactionsState.kt | 2 +- .../factory/TokenDetailsSwapTransactionsStateConverter.kt | 4 ++-- .../ui/components/exchange/ExchangeStatusBottomSheet.kt | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index 11d4b21719..5e42550ab6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -29,7 +29,7 @@ internal data class SwapTransactionsState( val fromFiatAmount: String, val fromCurrencyIcon: TokenIconState, val onClick: () -> Unit, - val onGoToProviderClick: () -> Unit, + val onGoToProviderClick: (String) -> Unit, ) internal class ExchangeStatusState( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 1c53790afa..0c6b75dfed 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -96,11 +96,11 @@ internal class TokenDetailsSwapTransactionsStateConverter( fromFiatAmount = getFiatAmount(fromFiatAmount), fromCurrencyIcon = iconStateConverter.convert(fromCurrency), onClick = { clickIntents.onSwapTransactionClick(transaction.txId, transaction.status?.status) }, - onGoToProviderClick = { + onGoToProviderClick = { url -> analyticsEventsHandlerProvider().send( TokenExchangeAnalyticsEvent.GoToProviderStatus(cryptoCurrency.symbol), ) - clickIntents.onGoToProviderClick(url = transaction.status?.txUrl.orEmpty()) + clickIntents.onGoToProviderClick(url = url) }, ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index 743b214df8..f8850738e2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -1,7 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -78,8 +77,8 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC SpacerH12() ExchangeStatusBlock( statuses = config.statuses, - showLink = notification.value == null, - onClick = config.onGoToProviderClick, + showLink = notification.value == null && config.txUrl != null, + onClick = { config.onGoToProviderClick(config.txUrl.orEmpty()) }, ) AnimatedContent( targetState = notification.value, From f257b5758d5be063d679e42ca6ea33d11a291641 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 15:29:40 +0300 Subject: [PATCH 117/139] Updated on 2026-08-14 --- .../state/factory/TokenDetailsSwapTransactionsStateConverter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 0c6b75dfed..b67c23da81 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -194,6 +194,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( status = ExchangeStatus.New, text = when { isNew -> TextReference.Res(R.string.express_exchange_status_receiving_active) + isNewDone -> TextReference.Res(R.string.express_exchange_status_received) else -> TextReference.Res(R.string.express_exchange_status_receiving) }, isActive = isNew, From 5d381ed902ea78149d581230f2d935637d5fdc88 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 16:10:36 +0300 Subject: [PATCH 118/139] Updated on 2026-08-14 --- .../tangem/feature/swap/ui/StateBuilder.kt | 30 +++++++++++++++++++ .../feature/swap/viewmodels/SwapViewModel.kt | 7 +++++ 2 files changed, 37 insertions(+) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6d540ed412..fb166fb06c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -784,6 +784,36 @@ internal class StateBuilder( ) } + fun updateProvidersBottomSheetContent( + uiState: SwapStateHolder, + tokenSwapInfoForProviders: Map, + ): SwapStateHolder { + val config = uiState.bottomSheetConfig?.content as? ChooseProviderBottomSheetConfig + return if (config != null) { + val providers = config.providers + uiState.copy( + bottomSheetConfig = uiState.bottomSheetConfig.copy( + content = config.copy( + providers = providers.map { + val tokenInfo = tokenSwapInfoForProviders[it.id] + if (it is ProviderState.Content && tokenInfo != null) { + val rateString = tokenInfo.tokenAmount + .getFormattedCryptoAmount(tokenInfo.cryptoCurrencyStatus.currency) + it.copy( + subtitle = stringReference(rateString), + ) + } else { + it + } + }.toImmutableList(), + ), + ), + ) + } else { + uiState + } + } + fun updateSelectedProvider(uiState: SwapStateHolder, selectedProviderId: String): SwapStateHolder { val config = uiState.bottomSheetConfig?.content as? ChooseProviderBottomSheetConfig return if (config != null) { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 64afcb59a5..176ca7d775 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -271,6 +271,13 @@ internal class SwapViewModel @Inject constructor( if (providersState.isNotEmpty()) { val (provider, state) = updateLoadedQuotes(providersState) setupLoadedState(provider, state, fromToken) + uiState = stateBuilder.updateProvidersBottomSheetContent( + uiState = uiState, + tokenSwapInfoForProviders = providersState + .getLastLoadedSuccessStates() + .entries + .associate { it.key.providerId to it.value.toTokenInfo }, + ) } else { Timber.e("Accidentally empty quotes list") } From f750267a774a9b0f0e8e96b3d825c27b187acbe4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 16:24:05 +0300 Subject: [PATCH 119/139] Updated on 2026-08-14 --- .../com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 0e3c786b0a..a9ba221873 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -804,6 +804,13 @@ internal class SwapInteractorImpl @Inject constructor( if (fromToken is CryptoCurrency.Token) { return IncludeFeeInAmount.Excluded } + + val tokenForFeeBalance = + userWalletManager.getNativeTokenBalance(networkId, derivationPath) ?: ProxyAmount.empty() + + if (amount.value > tokenForFeeBalance.value) { + return IncludeFeeInAmount.BalanceNotEnough + } val feeValue = when (txFee) { TxFeeState.Empty -> BigDecimal.ZERO is TxFeeState.MultipleFeeState -> if (selectedFee == FeeType.NORMAL) { @@ -814,8 +821,6 @@ internal class SwapInteractorImpl @Inject constructor( is TxFeeState.SingleFeeState -> txFee.fee.feeValue } - val tokenForFeeBalance = - userWalletManager.getNativeTokenBalance(networkId, derivationPath) ?: ProxyAmount.empty() val amountWithFee = amount.value + feeValue return if (amountWithFee < tokenForFeeBalance.value) { IncludeFeeInAmount.Excluded From 8cbd43175aaa72f3b3b7accff03869b456b9433d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 16:25:53 +0300 Subject: [PATCH 120/139] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 176ca7d775..3e47a29bf3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -490,6 +490,7 @@ internal class SwapViewModel @Inject constructor( when (it) { is TxState.TxSent -> { uiState = stateBuilder.loadingPermissionState(uiState) + uiState = stateBuilder.dismissBottomSheet(uiState) } is TxState.UserCancelled -> Unit else -> { From 87e327a2204999c1a3e2b9efefabe7f37b8e4145 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 16:33:25 +0300 Subject: [PATCH 121/139] Updated on 2026-08-14 --- .../transaction/error/SendTransactionError.kt | 9 +++++ .../usecase/SendTransactionUseCase.kt | 36 ++++++++++++++++++- .../feature/swap/domain/SwapInteractorImpl.kt | 5 +-- .../tangem/feature/swap/ui/StateBuilder.kt | 2 +- .../feature/swap/viewmodels/SwapViewModel.kt | 4 +-- 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt index 26fdceff5a..8ed9705902 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/SendTransactionError.kt @@ -7,4 +7,13 @@ sealed class SendTransactionError { data class DataError(val message: String?) : SendTransactionError() data class NetworkError(val message: String?) : SendTransactionError() + + data class BlockchainSdkError(val code: Int, val cause: Throwable?) : SendTransactionError() + object UserCancelledError : SendTransactionError() + data class TangemSdkError(val code: Int, val cause: Throwable?) : SendTransactionError() + data class UnknownError(val ex: Exception? = null) : SendTransactionError() + + companion object { + const val USER_CANCELLED_ERROR_CODE = 50002 + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 0a289bfcdc..6860e413c9 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -3,13 +3,17 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import arrow.core.left import arrow.core.right +import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.network.ResultChecker +import com.tangem.common.core.TangemSdkError import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet @@ -52,10 +56,40 @@ class SendTransactionUseCase( ifRight = { result -> when (result) { is SimpleResult.Success -> true.right() - is SimpleResult.Failure -> SendTransactionError.NetworkError(result.error.message).left() + is SimpleResult.Failure -> handleError(result).left() } }, ifLeft = { it.left() }, ) } + + private fun handleError(result: SimpleResult.Failure): SendTransactionError { + if (ResultChecker.isNetworkError(result)) return SendTransactionError.NetworkError(result.error.message) + val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError() + when (error) { + is BlockchainSdkError.WrappedTangemError -> { + val errorByCode = mapErrorByCode(error) + if (errorByCode != null) { + return errorByCode + } + val tangemSdkError = error.tangemError as? TangemSdkError ?: return SendTransactionError.UnknownError() + if (tangemSdkError is TangemSdkError.UserCancelled) return SendTransactionError.UserCancelledError + return SendTransactionError.TangemSdkError(tangemSdkError.code, tangemSdkError.cause) + } + else -> { + return SendTransactionError.TangemSdkError(error.code, error.cause) + } + } + } + + private fun mapErrorByCode(error: BlockchainSdkError.WrappedTangemError): SendTransactionError? { + return when (error.code) { + USER_CANCELLED_ERROR_CODE -> { + return SendTransactionError.UserCancelledError + } + else -> { + null + } + } + } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 0e3c786b0a..118f753dc5 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -502,9 +502,10 @@ internal class SwapInteractorImpl @Inject constructor( return result.fold( ifLeft = { when (it) { + SendTransactionError.UserCancelledError -> TxState.UserCancelled + is SendTransactionError.BlockchainSdkError -> TxState.BlockchainError + is SendTransactionError.TangemSdkError -> TxState.TangemSdkError is SendTransactionError.NetworkError -> TxState.NetworkError - is SendTransactionError.DataError -> TxState.BlockchainError - SendTransactionError.DemoCardError -> TxState.UnknownError else -> TxState.UnknownError } }, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6d540ed412..625f477f4a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -825,7 +825,7 @@ internal class StateBuilder( ) } - fun updateSelectedFee(uiState: SwapStateHolder, selectedFee: FeeType): SwapStateHolder { + fun updateSelectedFeeBottomSheet(uiState: SwapStateHolder, selectedFee: FeeType): SwapStateHolder { val config = uiState.bottomSheetConfig?.content as? ChooseFeeBottomSheetConfig return if (config != null) { uiState.copy( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 64afcb59a5..7f2ce1b8ed 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -294,7 +294,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromToken.currency, swapProvider = provider, bestRatedProviderId = bestRatedProviderId, - isManyProviders = dataState.lastLoadedSwapStates.isNotEmpty(), + isManyProviders = dataState.lastLoadedSwapStates.size > 1, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) } @@ -717,7 +717,7 @@ internal class SwapViewModel @Inject constructor( val fromToken = dataState.fromCryptoCurrency ?: return@UiActions val amountToSwap = dataState.amount ?: return@UiActions val selectedProvider = dataState.selectedProvider ?: return@UiActions - uiState = stateBuilder.updateSelectedFee(uiState, it.feeType) + uiState = stateBuilder.updateSelectedFeeBottomSheet(uiState, it.feeType) dataState = dataState.copy(selectedFee = it) viewModelScope.launch(dispatchers.io) { val updatedState = swapInteractor.updateQuotesStateWithSelectedFee( From a9dfa54eec9cbcb961a377c3593dc7313cb61c5e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Dec 2023 13:52:13 +0300 Subject: [PATCH 122/139] Updated on 2026-08-14 --- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index d5f89f57f5..ff04eae099 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -97,12 +97,8 @@ class GetCryptoCurrencyActionsUseCase( // copy address activeList.add(TokenActionsState.ActionState.CopyAddress(true)) - // buy - if (rampManager.availableForBuy(cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Buy(true)) - } else { - disabledList.add(TokenActionsState.ActionState.Buy(false)) - } + // receive + activeList.add(TokenActionsState.ActionState.Receive(true)) // send if (isSendDisabled(cryptoCurrencyStatus = cryptoCurrencyStatus, coinStatus = coinStatus)) { @@ -111,9 +107,6 @@ class GetCryptoCurrencyActionsUseCase( activeList.add(TokenActionsState.ActionState.Send(true)) } - // receive - activeList.add(TokenActionsState.ActionState.Receive(true)) - // swap if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency.id)) { activeList.add(TokenActionsState.ActionState.Swap(true)) @@ -121,6 +114,13 @@ class GetCryptoCurrencyActionsUseCase( disabledList.add(TokenActionsState.ActionState.Swap(false)) } + // buy + if (rampManager.availableForBuy(cryptoCurrency)) { + activeList.add(TokenActionsState.ActionState.Buy(true)) + } else { + disabledList.add(TokenActionsState.ActionState.Buy(false)) + } + // sell if (rampManager.availableForSell(cryptoCurrency)) { activeList.add(TokenActionsState.ActionState.Sell(true)) From f457baeddb04154fa7949087249fc9833382cb46 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 15:41:18 +0200 Subject: [PATCH 123/139] Updated on 2026-08-14 --- .../repository/DefaultCurrenciesRepository.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 06e956fa49..038d1678cf 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -390,13 +390,16 @@ internal class DefaultCurrenciesRepository( ) } - val response = tangemExpressApi.getAssets( - AssetsRequestBody( - tokensList = tokensList, - ), - ) + if (tokensList.isNotEmpty()) { + val response = tangemExpressApi.getAssets( + AssetsRequestBody( + tokensList = tokensList, + ), + ) + + assetsStore.store(userWalletId, response.getOrThrow()) + } - assetsStore.store(userWalletId, response.getOrThrow()) } catch (e: Throwable) { Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") } From e15683a79b275c537b9bf2ad90fedfd50fe66da2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 18:03:41 +0300 Subject: [PATCH 124/139] Updated on 2026-08-14 --- .../di/SwapTransactionStatusStoreModule.kt | 23 +++++++++++ .../DefaultSwapTransactionStatusStore.kt | 12 ++++++ .../swaptx/SwapTransactionStatusStore.kt | 18 +++++++++ .../repository/DefaultCurrenciesRepository.kt | 1 - .../analytics/TokenExchangeAnalyticsEvent.kt | 18 ++------- features/tokendetails/impl/build.gradle.kts | 1 + ...enDetailsSwapTransactionsStateConverter.kt | 8 +--- .../viewmodels/ExchangeStatusFactory.kt | 38 ++++++++++++++++++- .../viewmodels/TokenDetailsClickIntents.kt | 3 +- .../viewmodels/TokenDetailsViewModel.kt | 23 +++-------- 10 files changed, 102 insertions(+), 43 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/SwapTransactionStatusStoreModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/swaptx/DefaultSwapTransactionStatusStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SwapTransactionStatusStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SwapTransactionStatusStoreModule.kt new file mode 100644 index 0000000000..25f71c60a0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/SwapTransactionStatusStoreModule.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.swaptx.DefaultSwapTransactionStatusStore +import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SwapTransactionStatusStoreModule { + + @Provides + @Singleton + fun provideSwapTransactionStatusStore(): SwapTransactionStatusStore { + return DefaultSwapTransactionStatusStore( + dataStore = RuntimeDataStore(), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/DefaultSwapTransactionStatusStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/DefaultSwapTransactionStatusStore.kt new file mode 100644 index 0000000000..50b4f08c9d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/DefaultSwapTransactionStatusStore.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.local.swaptx + +import com.tangem.datasource.local.datastore.core.StringKeyDataStore + +internal class DefaultSwapTransactionStatusStore( + private val dataStore: StringKeyDataStore, +) : SwapTransactionStatusStore, StringKeyDataStore by dataStore { + + override suspend fun getTransactionStatus(txId: String) = getSyncOrNull(txId) + + override suspend fun setTransactionStatus(txId: String, status: ExchangeAnalyticsStatus) = store(txId, status) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt new file mode 100644 index 0000000000..59bd30030e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.local.swaptx + +/** + * Runtime cache for storing swap transactions statuses sent to analytics + */ +interface SwapTransactionStatusStore { + suspend fun getTransactionStatus(txId: String): ExchangeAnalyticsStatus? + + suspend fun setTransactionStatus(txId: String, status: ExchangeAnalyticsStatus) +} + +enum class ExchangeAnalyticsStatus(val value: String) { + InProgress("In Progress"), + Done("Done"), + Fail("Fail"), + KYC("KYC"), + Refunded("Refunded"), +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 038d1678cf..d927d3e04e 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -399,7 +399,6 @@ internal class DefaultCurrenciesRepository( assetsStore.store(userWalletId, response.getOrThrow()) } - } catch (e: Throwable) { Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenExchangeAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenExchangeAnalyticsEvent.kt index a795ccd2af..a2161adc46 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenExchangeAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenExchangeAnalyticsEvent.kt @@ -7,26 +7,16 @@ class TokenExchangeAnalyticsEvent( params: Map = mapOf(), ) : AnalyticsEvent("Token", event, params, null) { - class CexTx(token: String) : TokenScreenAnalyticsEvent( - event = "Notice - ChangeNow Swap", + class CexTxStatusOpened(token: String) : TokenScreenAnalyticsEvent( + event = "ChangeNow Status Opened", params = mapOf("Token" to token), ) - class CexTxOpened(token: String, status: String) : TokenScreenAnalyticsEvent( - event = "ChangeNow Swap Opened", + class CexTxStatusChanged(token: String, status: String) : TokenScreenAnalyticsEvent( + event = "ChangeNow Status", params = mapOf("Token" to token, "Status" to status), ) - class Verification(token: String) : TokenScreenAnalyticsEvent( - event = "Notice - KYC required", - params = mapOf("Token" to token), - ) - - class Fail(token: String) : TokenScreenAnalyticsEvent( - event = "Notice - Operation Fail", - params = mapOf("Token" to token), - ) - class GoToProviderStatus(token: String) : TokenScreenAnalyticsEvent( event = "Button - Go To Provider", params = mapOf("Token" to token, "Place" to "Status"), diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index d7592edba4..8c4c106fa0 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.analytics) implementation(projects.core.analytics.models) + implementation(projects.core.datasource) /** Domain modules */ implementation(projects.domain.appCurrency) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index b67c23da81..3650c86759 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -95,7 +95,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( fromCryptoSymbol = fromCurrency.currency.symbol, fromFiatAmount = getFiatAmount(fromFiatAmount), fromCurrencyIcon = iconStateConverter.convert(fromCurrency), - onClick = { clickIntents.onSwapTransactionClick(transaction.txId, transaction.status?.status) }, + onClick = { clickIntents.onSwapTransactionClick(transaction.txId) }, onGoToProviderClick = { url -> analyticsEventsHandlerProvider().send( TokenExchangeAnalyticsEvent.GoToProviderStatus(cryptoCurrency.symbol), @@ -130,9 +130,6 @@ internal class TokenDetailsSwapTransactionsStateConverter( if (txUrl == null) return null return when (status) { ExchangeStatus.Failed -> { - analyticsEventsHandlerProvider().send( - TokenExchangeAnalyticsEvent.Fail(cryptoCurrency.symbol), - ) ExchangeStatusNotifications.Failed { analyticsEventsHandlerProvider().send( TokenExchangeAnalyticsEvent.GoToProviderFail(cryptoCurrency.symbol), @@ -141,9 +138,6 @@ internal class TokenDetailsSwapTransactionsStateConverter( } } ExchangeStatus.Verifying -> { - analyticsEventsHandlerProvider().send( - TokenExchangeAnalyticsEvent.Verification(cryptoCurrency.symbol), - ) ExchangeStatusNotifications.NeedVerification { analyticsEventsHandlerProvider().send( TokenExchangeAnalyticsEvent.GoToProviderKYC(cryptoCurrency.symbol), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index 4e7fc63875..13445dc76d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -3,10 +3,13 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels import arrow.core.getOrElse import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.datasource.local.swaptx.ExchangeAnalyticsStatus +import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.SwapRepository @@ -33,6 +36,7 @@ internal class ExchangeStatusFactory( private val swapRepository: SwapRepository, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val swapTransactionStatusStore: SwapTransactionStatusStore, private val dispatchers: CoroutineDispatcherProvider, private val clickIntents: TokenDetailsClickIntents, private val appCurrencyProvider: Provider, @@ -90,10 +94,26 @@ internal class ExchangeStatusFactory( return swapRepository.getExchangeStatus(txId) .fold( ifLeft = { null }, - ifRight = { it }, + ifRight = { + sendStatusUpdateAnalytics(it) + it + }, ) } + private suspend fun sendStatusUpdateAnalytics(statusModel: ExchangeStatusModel) { + val txId = statusModel.txId ?: return + val status = toAnalyticStatus(statusModel.status) ?: return + val savedStatus = swapTransactionStatusStore.getTransactionStatus(txId) + + if (savedStatus != status) { + analyticsEventsHandlerProvider().send( + TokenExchangeAnalyticsEvent.CexTxStatusChanged(cryptoCurrency.symbol, status.value), + ) + swapTransactionStatusStore.setTransactionStatus(txId, status) + } + } + private fun getExchangeStatusState( savedTransactions: List?, cryptoCurrencyStatusList: List, @@ -123,4 +143,20 @@ internal class ExchangeStatusFactory( this } } + + private fun toAnalyticStatus(status: ExchangeStatus?): ExchangeAnalyticsStatus? { + return when (status) { + ExchangeStatus.New, + ExchangeStatus.Waiting, + ExchangeStatus.Sending, + ExchangeStatus.Confirming, + ExchangeStatus.Exchanging, + -> ExchangeAnalyticsStatus.InProgress + ExchangeStatus.Verifying -> ExchangeAnalyticsStatus.KYC + ExchangeStatus.Failed -> ExchangeAnalyticsStatus.Fail + ExchangeStatus.Finished -> ExchangeAnalyticsStatus.Done + ExchangeStatus.Refunded -> ExchangeAnalyticsStatus.Refunded + else -> null + } + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 64a0d6dd2e..95093f7fdb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -2,7 +2,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.feature.swap.domain.models.domain.ExchangeStatus interface TokenDetailsClickIntents { @@ -40,7 +39,7 @@ interface TokenDetailsClickIntents { fun onCloseRentInfoNotification() - fun onSwapTransactionClick(txId: String, status: ExchangeStatus?) + fun onSwapTransactionClick(txId: String) fun onGoToProviderClick(url: String) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 62b23a3763..1a718c9846 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.resourceReference +import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -36,11 +37,9 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.domain.SwapRepository import com.tangem.feature.swap.domain.SwapTransactionRepository -import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.features.tokendetails.impl.R import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -77,6 +76,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, + private val swapTransactionStatusStore: SwapTransactionStatusStore, private val isDemoCardUseCase: IsDemoCardUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, @@ -116,6 +116,7 @@ internal class TokenDetailsViewModel @Inject constructor( swapRepository = swapRepository, getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase, + swapTransactionStatusStore = swapTransactionStatusStore, dispatchers = dispatchers, clickIntents = this, appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, @@ -215,9 +216,6 @@ internal class TokenDetailsViewModel @Inject constructor( swapTxStatusTaskScheduler.cancelTask() exchangeStatusFactory.invoke() .onEach { swapTxs -> - if (swapTxs.isNotEmpty()) { - analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.CexTx(cryptoCurrency.symbol)) - } swapTxStatusTaskScheduler.scheduleTask( viewModelScope, PeriodicTask( @@ -539,20 +537,9 @@ internal class TokenDetailsViewModel @Inject constructor( uiState = stateFactory.getStateWithRemovedRentNotification() } - override fun onSwapTransactionClick(txId: String, status: ExchangeStatus?) { + override fun onSwapTransactionClick(txId: String) { val swapTxState = uiState.swapTxs.first { it.txId == txId } - analyticsEventsHandler.send( - TokenExchangeAnalyticsEvent.CexTxOpened(cryptoCurrency.symbol, status?.name.orEmpty()), - ) - when (swapTxState.notification.value) { - is ExchangeStatusNotifications.NeedVerification -> { - analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.Verification(cryptoCurrency.symbol)) - } - is ExchangeStatusNotifications.Failed -> { - analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.Fail(cryptoCurrency.symbol)) - } - else -> Unit - } + analyticsEventsHandler.send(TokenExchangeAnalyticsEvent.CexTxStatusOpened(cryptoCurrency.symbol)) uiState = stateFactory.getStateWithExchangeStatusBottomSheet(swapTxState) } From fa1a34efa97b57dfebf1305540e467ff4d61136f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 18:35:00 +0300 Subject: [PATCH 125/139] Updated on 2026-08-14 --- .../repository/DefaultCurrenciesRepository.kt | 1 - .../tokens/GetCryptoCurrencyActionsUseCase.kt | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 038d1678cf..d927d3e04e 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -399,7 +399,6 @@ internal class DefaultCurrenciesRepository( assetsStore.store(userWalletId, response.getOrThrow()) } - } catch (e: Throwable) { Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index b0f110d438..87dee2d3d9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens +import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.model.CryptoCurrency @@ -55,6 +56,7 @@ class GetCryptoCurrencyActionsUseCase( userWalletId = userWallet.walletId, coinStatus = maybeCoinStatus.getOrNull(), cryptoCurrencyStatus = cryptoCurrencyStatus, + cardTypesResolver = userWallet.scanResponse.cardTypesResolver, ) } @@ -66,11 +68,17 @@ class GetCryptoCurrencyActionsUseCase( userWalletId: UserWalletId, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, + cardTypesResolver: CardTypesResolver, ): TokenActionsState { return TokenActionsState( walletId = userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, - states = createListOfActions(userWalletId, coinStatus, cryptoCurrencyStatus), + states = createListOfActions( + userWalletId, + coinStatus, + cryptoCurrencyStatus, + cardTypesResolver, + ), ) } @@ -82,6 +90,7 @@ class GetCryptoCurrencyActionsUseCase( userWalletId: UserWalletId, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, + cardTypesResolver: CardTypesResolver, ): List { val cryptoCurrency = cryptoCurrencyStatus.currency if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) { @@ -107,8 +116,11 @@ class GetCryptoCurrencyActionsUseCase( activeList.add(TokenActionsState.ActionState.Send(true)) } + val isMulticurrencyWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2() // swap - if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency)) { + if (isMulticurrencyWallet && + marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency) + ) { activeList.add(TokenActionsState.ActionState.Swap(true)) } else { disabledList.add(TokenActionsState.ActionState.Swap(false)) From 521305d2b71c8eee792a21727704b07935a05ab1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 17:35:18 +0200 Subject: [PATCH 126/139] Updated on 2026-08-14 --- .../domain/models/ui/TokensDataStateExpress.kt | 10 +++++++++- .../com/tangem/feature/swap/ui/StateBuilder.kt | 18 ++++++++++++++++++ .../feature/swap/viewmodels/SwapViewModel.kt | 13 +++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index b2c0d0c359..1d29633fec 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -5,7 +5,15 @@ import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo data class TokensDataStateExpress( val fromGroup: CurrenciesGroup, val toGroup: CurrenciesGroup, -) +) { + companion object { + val EMPTY = + TokensDataStateExpress( + fromGroup = CurrenciesGroup(emptyList(), emptyList()), + toGroup = CurrenciesGroup(emptyList(), emptyList()), + ) + } +} data class CurrenciesGroup( val available: List, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 0f31b4c6d7..bb0f7e892c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -567,6 +567,24 @@ internal class StateBuilder( } } + fun createInitialErrorState(uiState: SwapStateHolder, onRefreshClick: () -> Unit): SwapStateHolder { + return uiState.copy( + warnings = listOf( + SwapWarning.GeneralWarning( + notificationConfig = NotificationConfig( + title = TextReference.Res(R.string.warning_express_refresh_required_title), + subtitle = TextReference.EMPTY, + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = TextReference.Res(R.string.warning_button_refresh), + onClick = onRefreshClick, + ), + ), + ), + ), + ) + } + private fun createFeeState(txFeeState: TxFeeState, feeType: FeeType): FeeItemState { val isClickable: Boolean val fee = when (txFeeState) { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index c87574b344..631bdb18a8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -165,6 +165,19 @@ internal class SwapViewModel @Inject constructor( ) }.onFailure { Timber.tag(loggingTag).e(it) + + applyInitialTokenChoice( + state = TokensDataStateExpress.EMPTY, + selectedCurrency = null, + ) + + uiState = stateBuilder.createInitialErrorState(uiState) { + uiState = stateBuilder.createInitialLoadingState( + initialCurrency = initialCryptoCurrency, + networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId), + ) + initTokens() + } } } } From 9647e6caa370af8bd50fb3a1298b605d581081ee Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 13:19:03 +0400 Subject: [PATCH 127/139] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../converters/TopUpEventConverter.kt | 10 +-- .../common/analytics/events/AnalyticsParam.kt | 2 +- .../tap/common/analytics/events/Basic.kt | 15 +++-- .../common/analytics/topup/TopUpController.kt | 2 +- .../tap/di/domain/AnalyticsDomainModule.kt | 18 ++++++ .../features/onboarding/OnboardingHelper.kt | 12 ++++ .../note/redux/OnboardingNoteMiddleware.kt | 2 + .../twins/redux/TwinCardsMiddleware.kt | 2 + core/analytics/build.gradle.kts | 10 ++- .../core/analytics/models/AnalyticsParam.kt | 5 ++ .../core/analytics/di/AnalyticsModule.kt | 2 +- .../analytics/filter/OneTimeEventFilter.kt | 2 +- .../repository/AnalyticsRepository.kt | 8 --- .../local/preferences/AppPreferencesStore.kt | 18 +++++- .../local/preferences/PreferencesKeys.kt | 2 + .../utils/AppPreferencesStoreExt.kt | 24 ++++++- data/analytics/build.gradle.kts | 5 +- .../analytics/DefaultAnalyticsRepository.kt | 29 ++++++++- .../data/analytics/di/AnalyticsDataModule.kt | 2 +- domain/analytics/.gitignore | 1 + domain/analytics/build.gradle.kts | 12 ++++ .../analytics/CheckIsWalletToppedUpUseCase.kt | 56 ++++++++++++++++ .../analytics/model/WalletBalanceState.kt | 5 ++ .../repository/AnalyticsRepository.kt | 15 +++++ features/wallet/impl/build.gradle.kts | 1 + .../analytics/WalletScreenAnalyticsEvent.kt | 12 ++++ .../utils/TokenListAnalyticsSender.kt | 64 ++++++++++++++++--- .../SingleWalletWithTokenListSubscriber.kt | 2 +- .../wallet/subscribers/TokenListSubscriber.kt | 2 +- settings.gradle.kts | 1 + 31 files changed, 302 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/AnalyticsDomainModule.kt delete mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt create mode 100644 domain/analytics/.gitignore create mode 100644 domain/analytics/build.gradle.kts create mode 100644 domain/analytics/src/main/kotlin/com/tangem/domain/analytics/CheckIsWalletToppedUpUseCase.kt create mode 100644 domain/analytics/src/main/kotlin/com/tangem/domain/analytics/model/WalletBalanceState.kt create mode 100644 domain/analytics/src/main/kotlin/com/tangem/domain/analytics/repository/AnalyticsRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a3a05911c7..de5864d8b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.transaction) + implementation(projects.domain.analytics) implementation(projects.common) implementation(projects.core.analytics) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/TopUpEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/TopUpEventConverter.kt index 429b2e5ebc..ba62707b7f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/TopUpEventConverter.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/TopUpEventConverter.kt @@ -2,16 +2,18 @@ package com.tangem.tap.common.analytics.converters import com.tangem.common.Converter import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.common.analytics.events.Basic /** [REDACTED_AUTHOR] */ -class TopUpEventConverter : Converter { +class TopUpEventConverter : Converter, Basic.ToppedUp?> { - override fun convert(value: CardTypesResolver): Basic.ToppedUp? { - val paramCardCurrency = ParamCardCurrencyConverter().convert(value) ?: return null + override fun convert(value: Pair): Basic.ToppedUp? { + val (userWalletId, resolver) = value + val paramCardCurrency = ParamCardCurrencyConverter().convert(resolver) ?: return null - return Basic.ToppedUp(paramCardCurrency) + return Basic.ToppedUp(userWalletId, paramCardCurrency) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 2d1bd09138..21dcd6c657 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -14,7 +14,7 @@ sealed class AnalyticsParam { // MultiCurrency or CurrencyType sealed class CardCurrency(val value: String) { - object MultiCurrency : CardCurrency("Multicurrency") + object MultiCurrency : CardCurrency(value = "Multicurrency") class SingleCurrency(type: CurrencyType) : CardCurrency(type.value) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt index ade60b95cf..812527539e 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt @@ -1,6 +1,8 @@ package com.tangem.tap.common.analytics.events import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.OneTimeAnalyticsEvent +import com.tangem.domain.wallets.models.UserWalletId /** [REDACTED_AUTHOR] @@ -50,10 +52,15 @@ sealed class Basic( } } - class ToppedUp(currency: AnalyticsParam.CardCurrency) : Basic( - event = "Topped up", - params = mapOf(AnalyticsParam.CURRENCY to currency.value), - ) + class ToppedUp(userWalletId: UserWalletId, currency: AnalyticsParam.CardCurrency) : + Basic( + event = "Topped up", + params = mapOf(AnalyticsParam.CURRENCY to currency.value), + ), + OneTimeAnalyticsEvent { + + override val oneTimeEventId: String = id + userWalletId.stringValue + } class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom, memoType: MemoType) : Basic( event = "Transaction sent", diff --git a/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt b/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt index b32af636df..376143ec6b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt @@ -154,7 +154,7 @@ class TopUpController( if (cardBalanceState.isToppedUp()) { topupWalletStorage.save(topupInfo.copy(cardBalanceState = DataSourceTopupInfo.CardBalanceState.Full)) - TopUpEventConverter().convert(cardTypesResolver)?.let { + TopUpEventConverter().convert(value = userWalletId to cardTypesResolver)?.let { Analytics.send(it) } } diff --git a/app/src/main/java/com/tangem/tap/di/domain/AnalyticsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AnalyticsDomainModule.kt new file mode 100644 index 0000000000..dda9f2c253 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/AnalyticsDomainModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase +import com.tangem.domain.analytics.repository.AnalyticsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +internal object AnalyticsDomainModule { + + @Provides + fun provideCheckIsWalletToppedUpUseCase(analyticsRepository: AnalyticsRepository): CheckIsWalletToppedUpUseCase { + return CheckIsWalletToppedUpUseCase(analyticsRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 947a437152..2f455c7eae 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -11,7 +11,10 @@ import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.tap.* +import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.extensions.removeContext @@ -117,6 +120,15 @@ object OnboardingHelper { Analytics.removeContext() } + fun sendToppedUpEvent(scanResponse: ScanResponse) { + val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() + val currency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver) + + if (userWalletId != null && currency != null) { + Analytics.send(Basic.ToppedUp(userWalletId, currency)) + } + } + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List?) { val userWallet = UserWalletBuilder(scanResponse) .backupCardsIds(backupCardsIds?.toSet()) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 675eb82581..266b627b65 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -152,6 +152,8 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch } is OnboardingNoteAction.Balance.Set -> { if (action.balance.balanceIsToppedUp()) { + OnboardingHelper.sendToppedUpEvent(scanResponse) + store.state.globalState.topUpController?.send(scanResponse, AnalyticsParam.CardBalanceState.Full) store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.Done)) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index 441798d55f..31072fcd23 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -260,6 +260,8 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } is TwinCardsAction.Balance.Set -> { if (action.balance.balanceIsToppedUp()) { + OnboardingHelper.sendToppedUpEvent(getScanResponse()) + store.state.globalState.topUpController?.send(getScanResponse(), AnalyticsParam.CardBalanceState.Full) store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done)) } diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index 663fb0f33e..9d05970191 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -10,9 +10,15 @@ dependencies { implementation(deps.hilt.core) kapt(deps.hilt.kapt) - /** Core shouldn't depends on core, but in case with utils and logging its necessary */ - implementation(projects.core.utils) + /** Analytics - Models */ implementation(projects.core.analytics.models) + /** Domain */ + implementation(projects.domain.analytics) + + /** Other */ implementation(deps.kotlin.coroutines) + + /** Core shouldn't depend on core, but in case with utils and logging its necessary */ + implementation(projects.core.utils) } \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 91b8eb6880..830d2621c2 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -114,6 +114,11 @@ sealed class AnalyticsParam { object SeedImport : WalletCreationType("Seed import") } + sealed class WalletType(val value: String) { + object MultiCurrency : WalletType(value = "Multicurrency") + class SingleCurrency(currencyName: String) : WalletType(currencyName) + } + companion object Key { const val BLOCKCHAIN = "blockchain" const val TOKEN = "Token" diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt b/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt index b4bb704732..13715236c5 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt @@ -3,7 +3,7 @@ package com.tangem.core.analytics.di import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.filter.OneTimeEventFilter -import com.tangem.core.analytics.repository.AnalyticsRepository +import com.tangem.domain.analytics.repository.AnalyticsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt b/core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt index c224eff75b..f7e9903e26 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/filter/OneTimeEventFilter.kt @@ -4,7 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventFilter import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.OneTimeAnalyticsEvent -import com.tangem.core.analytics.repository.AnalyticsRepository +import com.tangem.domain.analytics.repository.AnalyticsRepository class OneTimeEventFilter( private val analyticsRepository: AnalyticsRepository, diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt b/core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt deleted file mode 100644 index 859220527a..0000000000 --- a/core/analytics/src/main/java/com/tangem/core/analytics/repository/AnalyticsRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.core.analytics.repository - -interface AnalyticsRepository { - - suspend fun checkIsEventSent(eventId: String): Boolean - - suspend fun setIsEventSent(eventId: String) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt index ae7bb4ee8c..1619c322de 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt @@ -44,12 +44,20 @@ class AppPreferencesStore( return this[key]?.let(adapter::fromJson) } - /** Get nullable list of data [T] by string [key] */ + /** Get list of data [T] by string [key] */ inline fun MutablePreferences.getObjectList(key: Preferences.Key): List? { val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) return this[key]?.let(adapter::fromJson) } + /** Get map with [String] key and value [V] by string [key] from [MutablePreferences] */ + inline fun MutablePreferences.getObjectMap(key: Preferences.Key): Map? { + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) + + return this[key]?.let(adapter::fromJson) + } + /** * Set data [T] by string [key] to [MutablePreferences] * @@ -67,4 +75,12 @@ class AppPreferencesStore( val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) this[key] = adapter.toJson(value) } + + /** Set map with [String] key and value [V] by string [key] to [MutablePreferences] */ + inline fun MutablePreferences.setObjectMap(key: Preferences.Key, value: Map) { + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) + + this[key] = adapter.toJson(value) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 0ad9c10237..916027a8aa 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -36,6 +36,8 @@ object PreferencesKeys { val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") } val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") } + + val WALLETS_BALANCES_STATES_KEY by lazy { stringPreferencesKey(name = "walletsBalancesStates") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index 974b36147a..c0fc7e0cef 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -76,9 +76,31 @@ inline fun AppPreferencesStore.getObjectList(key: Preferences.Key AppPreferencesStore.getObjectListSync(key: Preferences.Key): List { val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) + return data.firstOrNull() + ?.get(key) + ?.let(adapter::fromJson) + .orEmpty() +} + +/** Store map with [String] key and value [V] by string [key] */ +suspend inline fun AppPreferencesStore.storeObjectMap( + key: Preferences.Key, + value: Map, +) { + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) + + edit { it[key] = adapter.toJson(value) } +} + +/** Get map with [String] key and value [V] by string [key], or empty if data is not found */ +suspend inline fun AppPreferencesStore.getObjectMap(key: Preferences.Key): Map { + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) + return data.firstOrNull() ?.get(key) ?.let(adapter::fromJson) diff --git a/data/analytics/build.gradle.kts b/data/analytics/build.gradle.kts index 258c49ea37..293c5cf974 100644 --- a/data/analytics/build.gradle.kts +++ b/data/analytics/build.gradle.kts @@ -11,8 +11,11 @@ android { dependencies { + /** Project - Domain */ + implementation(projects.domain.analytics) + implementation(projects.domain.wallets.models) + /** Project - Analytics */ - implementation(projects.core.analytics) implementation(projects.core.analytics.models) /** Project - Data */ diff --git a/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt b/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt index 9235045bae..4866aa1c89 100644 --- a/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt +++ b/data/analytics/src/main/kotlin/com/tangem/data/analytics/DefaultAnalyticsRepository.kt @@ -1,9 +1,12 @@ package com.tangem.data.analytics -import com.tangem.core.analytics.repository.AnalyticsRepository import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectListSync +import com.tangem.datasource.local.preferences.utils.getObjectMap +import com.tangem.domain.analytics.model.WalletBalanceState +import com.tangem.domain.analytics.repository.AnalyticsRepository +import com.tangem.domain.wallets.models.UserWalletId internal class DefaultAnalyticsRepository( private val appPreferencesStore: AppPreferencesStore, @@ -18,10 +21,30 @@ internal class DefaultAnalyticsRepository( override suspend fun setIsEventSent(eventId: String) { appPreferencesStore.editData { mutablePreferences -> - val sentEvents = mutablePreferences.getObject>(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY) + val sentEvents = mutablePreferences.getObjectList(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY) val updatedSentEvents = sentEvents.orEmpty() + eventId - mutablePreferences.setObject(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY, updatedSentEvents) + mutablePreferences.setObjectList(PreferencesKeys.SENT_ONE_TIME_EVENTS_KEY, updatedSentEvents) + } + } + + override suspend fun getWalletBalanceState(userWalletId: UserWalletId): WalletBalanceState? { + val walletsBalanceState = appPreferencesStore.getObjectMap( + key = PreferencesKeys.WALLETS_BALANCES_STATES_KEY, + ) + + return walletsBalanceState[userWalletId.stringValue] + } + + override suspend fun setWalletBalanceState(userWalletId: UserWalletId, balanceState: WalletBalanceState) { + appPreferencesStore.editData { + val walletsBalanceState = it.getObjectMap( + key = PreferencesKeys.WALLETS_BALANCES_STATES_KEY, + ) + val updatedWalletsBalanceState = walletsBalanceState.orEmpty() + .plus(pair = userWalletId.stringValue to balanceState) + + it.setObjectMap(PreferencesKeys.WALLETS_BALANCES_STATES_KEY, updatedWalletsBalanceState) } } } \ No newline at end of file diff --git a/data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt b/data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt index ed3e2b04ac..95fcd5add2 100644 --- a/data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt +++ b/data/analytics/src/main/kotlin/com/tangem/data/analytics/di/AnalyticsDataModule.kt @@ -1,8 +1,8 @@ package com.tangem.data.analytics.di -import com.tangem.core.analytics.repository.AnalyticsRepository import com.tangem.data.analytics.DefaultAnalyticsRepository import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.analytics.repository.AnalyticsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/domain/analytics/.gitignore b/domain/analytics/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/analytics/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/analytics/build.gradle.kts b/domain/analytics/build.gradle.kts new file mode 100644 index 0000000000..e5e871ea2f --- /dev/null +++ b/domain/analytics/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + + /** Project - Domain */ + implementation(projects.core.utils) + implementation(projects.domain.core) + implementation(projects.domain.wallets.models) +} \ No newline at end of file diff --git a/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/CheckIsWalletToppedUpUseCase.kt b/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/CheckIsWalletToppedUpUseCase.kt new file mode 100644 index 0000000000..3fc031a509 --- /dev/null +++ b/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/CheckIsWalletToppedUpUseCase.kt @@ -0,0 +1,56 @@ +package com.tangem.domain.analytics + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.analytics.model.WalletBalanceState +import com.tangem.domain.analytics.repository.AnalyticsRepository +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Check if wallet has been topped up now. + * */ +class CheckIsWalletToppedUpUseCase( + private val analyticsRepository: AnalyticsRepository, +) { + + /** + * Check if wallet has been topped up now. + * + * @param userWalletId User wallet ID. + * @param balanceState Current wallet balance. + * + * @return Either [Throwable] or [Boolean] representing if wallet has been topped up. + * */ + suspend operator fun invoke( + userWalletId: UserWalletId, + balanceState: WalletBalanceState, + ): Either = either { + val storedBalanceState = getStoredBalanceState(userWalletId) + + when { + storedBalanceState == WalletBalanceState.ToppedUp -> false // already topped up + storedBalanceState == null && balanceState == WalletBalanceState.ToppedUp -> { + setBalanceState(userWalletId, balanceState) + false // already topped up + } + else -> { + setBalanceState(userWalletId, balanceState) + balanceState == WalletBalanceState.ToppedUp + } + } + } + + private suspend fun Raise.getStoredBalanceState(userWalletId: UserWalletId) = catch( + block = { analyticsRepository.getWalletBalanceState(userWalletId) }, + catch = ::raise, + ) + + private suspend fun Raise.setBalanceState(userWalletId: UserWalletId, balanceState: WalletBalanceState) { + catch( + block = { analyticsRepository.setWalletBalanceState(userWalletId, balanceState) }, + catch = ::raise, + ) + } +} \ No newline at end of file diff --git a/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/model/WalletBalanceState.kt b/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/model/WalletBalanceState.kt new file mode 100644 index 0000000000..e9cde413a2 --- /dev/null +++ b/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/model/WalletBalanceState.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.analytics.model + +enum class WalletBalanceState { + ToppedUp, Empty, Error, +} \ No newline at end of file diff --git a/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/repository/AnalyticsRepository.kt b/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/repository/AnalyticsRepository.kt new file mode 100644 index 0000000000..2e80e2259c --- /dev/null +++ b/domain/analytics/src/main/kotlin/com/tangem/domain/analytics/repository/AnalyticsRepository.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.analytics.repository + +import com.tangem.domain.analytics.model.WalletBalanceState +import com.tangem.domain.wallets.models.UserWalletId + +interface AnalyticsRepository { + + suspend fun checkIsEventSent(eventId: String): Boolean + + suspend fun setIsEventSent(eventId: String) + + suspend fun getWalletBalanceState(userWalletId: UserWalletId): WalletBalanceState? + + suspend fun setWalletBalanceState(userWalletId: UserWalletId, balanceState: WalletBalanceState) +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index f96ee8b723..6c78faf07f 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -66,6 +66,7 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.analytics) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) 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 8d43c5d586..f244b3ffca 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 @@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.OneTimeAnalyticsEvent +import com.tangem.domain.wallets.models.UserWalletId sealed class WalletScreenAnalyticsEvent { @@ -11,6 +13,16 @@ sealed class WalletScreenAnalyticsEvent { error: Throwable? = null, ) : AnalyticsEvent(category = "Basic", event = event, params = params, error = error) { + class WalletToppedUp(userWalletId: UserWalletId, walletType: AnalyticsParam.WalletType) : + Basic( + event = "Topped up", + params = mapOf(AnalyticsParam.CURRENCY to walletType.value), + ), + OneTimeAnalyticsEvent { + + override val oneTimeEventId: String = id + userWalletId.stringValue + } + object WalletOpened : Basic(event = "Wallet Opened") class CardWasScanned(source: AnalyticsParam.ScannedFrom) : Basic( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index eb62eaf6e3..666480b385 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -1,13 +1,16 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils import arrow.core.Either -import com.tangem.common.extensions.isZero +import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase +import com.tangem.domain.analytics.model.WalletBalanceState import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import dagger.hilt.android.scopes.ViewModelScoped import java.math.BigDecimal @@ -16,13 +19,43 @@ import javax.inject.Inject @ViewModelScoped internal class TokenListAnalyticsSender @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, + private val checkIsWalletToppedUpUseCase: CheckIsWalletToppedUpUseCase, ) { - fun send(maybeTokenList: Either) { + suspend fun send(userWallet: UserWallet, maybeTokenList: Either) { val tokenList = (maybeTokenList as? Either.Right)?.value ?: return - createCardBalanceState(tokenList)?.let { - analyticsEventHandler.send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balance = it)) + sendBalanceLoadedEventIfNeeded(tokenList) + sendToppedUpEventIfNeeded(tokenList, userWallet) + } + + private fun sendBalanceLoadedEventIfNeeded(tokenList: TokenList) { + createCardBalanceState(tokenList)?.let { balanceState -> + analyticsEventHandler.send(event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded(balanceState)) + } + } + + private suspend fun sendToppedUpEventIfNeeded(tokenList: TokenList, userWallet: UserWallet) { + val balanceState = tokenList.toWalletBalanceState() ?: return + + val isWalletToppedUp = checkIsWalletToppedUpUseCase(userWallet.walletId, balanceState) + .getOrElse { return } + + if (isWalletToppedUp) { + val walletType = if (userWallet.isMultiCurrency) { + AnalyticsParam.WalletType.MultiCurrency + } else { + // For single currency wallets with token list, e.g. Noodle + val currency = when (tokenList) { + is TokenList.Empty -> return + is TokenList.GroupedByNetwork -> tokenList.groups.first().currencies.first() + is TokenList.Ungrouped -> tokenList.currencies.first() + } + + AnalyticsParam.WalletType.SingleCurrency(currency.currency.name) + } + + analyticsEventHandler.send(WalletScreenAnalyticsEvent.Basic.WalletToppedUp(userWallet.walletId, walletType)) } } @@ -30,10 +63,11 @@ internal class TokenListAnalyticsSender @Inject constructor( return when (val fiatBalance = tokenList.totalFiatBalance) { is TokenList.FiatBalance.Failed -> fiatBalance.toCardBalanceState(tokenList) is TokenList.FiatBalance.Loaded -> fiatBalance.toCardBalanceState() - TokenList.FiatBalance.Loading -> null + is TokenList.FiatBalance.Loading -> null } } + @Suppress("UnusedReceiverParameter") private fun TokenList.FiatBalance.Failed.toCardBalanceState(tokenList: TokenList): AnalyticsParam.CardBalanceState { val currenciesStatuses = when (tokenList) { is TokenList.Empty -> emptyList() @@ -50,13 +84,25 @@ internal class TokenListAnalyticsSender @Inject constructor( } } - private fun TokenList.FiatBalance.Loaded.toCardBalanceState(): AnalyticsParam.CardBalanceState? { + private fun TokenList.FiatBalance.Loaded.toCardBalanceState(): AnalyticsParam.CardBalanceState { return if (amount > BigDecimal.ZERO) { AnalyticsParam.CardBalanceState.Full - } else if (amount.isZero()) { - AnalyticsParam.CardBalanceState.Empty } else { - null + AnalyticsParam.CardBalanceState.Empty + } + } + + private fun TokenList.toWalletBalanceState(): WalletBalanceState? { + return when (val balance = totalFiatBalance) { + is TokenList.FiatBalance.Failed -> WalletBalanceState.Error + is TokenList.FiatBalance.Loaded -> { + if (balance.amount > BigDecimal.ZERO) { + WalletBalanceState.ToppedUp + } else { + WalletBalanceState.Empty + } + } + is TokenList.FiatBalance.Loading -> null } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index a6446944c0..64072bdc87 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -38,7 +38,7 @@ internal class SingleWalletWithTokenListSubscriber( .conflate() .distinctUntilChanged() .onEach(::updateContent) - .onEach(tokenListAnalyticsSender::send) + .onEach { tokenListAnalyticsSender.send(userWallet, maybeTokenList = it) } .onEach(walletWithFundsChecker::check) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt index 4c4caf4479..c8f2a72509 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListSubscriber.kt @@ -40,7 +40,7 @@ internal class TokenListSubscriber( .conflate() .distinctUntilChanged() .onEach(::updateContent) - .onEach(tokenListAnalyticsSender::send) + .onEach { tokenListAnalyticsSender.send(userWallet, maybeTokenList = it) } .onEach(walletWithFundsChecker::check) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1abc2626ae..3ab3775f15 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -128,6 +128,7 @@ include(":domain:app-theme:models") include(":domain:balance-hiding") include(":domain:balance-hiding:models") include(":domain:transaction") +include(":domain:analytics") // endregion Domain modules // region Data modules From 9e225711c66ba405b9588de50691790766a90f2b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 12:20:21 +0300 Subject: [PATCH 128/139] Updated on 2026-08-14 --- core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 93dae822ba..d2c890aad3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -157,7 +157,7 @@ private fun darkThemeColors(): TangemColors { ), button = TangemColors.Button( primary = TangemColorPalette.Light4, - secondary = TangemColorPalette.Dark5, + secondary = TangemColorPalette.Dark4, disabled = TangemColorPalette.Dark5, positiveDisabled = TangemColorPalette.DarkGreen, ), From 5d58362a47bb2693428c94ffe39370bc4e1c7433 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 12:20:56 +0300 Subject: [PATCH 129/139] Updated on 2026-08-14 --- .../src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 18d89a4d06..b16a5e4c21 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -443,6 +443,7 @@ fun ChangeTokenSelector() { Icon( modifier = Modifier.size(TangemTheme.dimens.size20), painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors.icon.secondary, contentDescription = null, ) } From af1f1ff9c0886b5a3a74bdb8076ce796205bb280 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 12:41:36 +0300 Subject: [PATCH 130/139] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index b13cd5c2fc..6b23060307 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-409" +tangemBlockchainSdk = "develop-411" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-312" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 75c70a3d77105ef7e4c531ed807aea7aa5d5782d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 13:52:30 +0200 Subject: [PATCH 131/139] Updated on 2026-08-14 --- .../feature/swap/analytics/SwapEvents.kt | 80 ++++- .../ui/SwapPermissionBottomSheetContent.kt | 297 ------------------ .../feature/swap/ui/SwapSelectTokenScreen.kt | 3 + .../feature/swap/viewmodels/SwapViewModel.kt | 73 ++++- 4 files changed, 135 insertions(+), 318 deletions(-) delete mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 768624019f..b128e88b19 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -1,6 +1,10 @@ package com.tangem.feature.swap.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.feature.swap.domain.models.ui.FeeType + +private const val SWAP_CATEGORY = "Swap" sealed class SwapEvents( event: String, @@ -13,14 +17,17 @@ sealed class SwapEvents( ) object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") - object ChooseTokenScreenOpened : SwapEvents(event = "Choose Token Screen Opened") - data class SearchTokenClicked(val currencySymbol: String?) : SwapEvents( - event = "Searched Token Clicked", + data class ChooseTokenScreenOpened(val availableTokens: Boolean) : SwapEvents( + event = "Choose Token Screen Opened", + params = mapOf("Available tokens" to if (availableTokens) "Yes" else "No"), + ) + + data class ChooseTokenScreenResult(val tokenChosen: Boolean, val token: String? = null) : SwapEvents( + event = "Choose Token Screen Result", params = buildMap { - if (currencySymbol != null) { - put("Token", currencySymbol) - } + put("Token Chosen", if (tokenChosen) "Yes" else "No") + token?.let { put("Token", it) } }, ) @@ -37,8 +44,61 @@ sealed class SwapEvents( ) object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") - object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") - object SwapInProgressScreen : SwapEvents(event = "Swap in Progress Screen Opened") -} -private const val SWAP_CATEGORY = "Swap" \ No newline at end of file + object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") + + data class SwapInProgressScreen( + val provider: SwapProvider, + val commission: FeeType, // Market / Fast + val sendToken: String, + val receiveToken: String, + ) : SwapEvents( + event = "Swap in Progress Screen Opened", + params = mapOf( + "Provider" to provider.name, + "Commission" to if (commission == FeeType.NORMAL) "Market" else "Fast", + "Send Token" to sendToken, + "Receive Token" to receiveToken, + ), + ) + + object ProviderClicked : SwapEvents("Provider Clicked") + + data class ProviderChosen(val provider: SwapProvider) : SwapEvents( + event = "Provider Chosen", + params = mapOf("Provider" to provider.name), + ) + + data class ButtonStatus(val token: String) : SwapEvents( + event = "Button - Status", + params = mapOf("Token" to token), + ) + + data class ButtonExplore(val token: String) : SwapEvents( + event = "Button - Explore", + params = mapOf("Token" to token), + ) + + object NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") + + data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( + event = "Notice - Not Enough Fee", + params = mapOf( + "Token" to token, + "Blockchain" to blockchain, + ), + ) + + data class NoticeProviderError( + val token: String, + val provider: SwapProvider, + val errorCode: Int, + ) : SwapEvents( + event = "Notice - Express Error", + params = mapOf( + "Token" to token, + "Provider" to provider.name, + "Error code" to errorCode.toString(), + ), + ) +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt deleted file mode 100644 index 617534b273..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt +++ /dev/null @@ -1,297 +0,0 @@ -package com.tangem.feature.swap.ui - -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import com.tangem.core.ui.components.* - -// @Composable -// fun SwapPermissionBottomSheetContent(data: SwapPermissionState.ReadyForRequest, onCancel: () -> Unit) { -// var isPermissionAlertShow by remember { mutableStateOf(false) } -// Column( -// modifier = Modifier -// .background(color = TangemTheme.colors.background.primary) -// .fillMaxWidth() -// .padding(horizontal = TangemTheme.dimens.spacing16), -// horizontalAlignment = Alignment.CenterHorizontally, -// ) { -// Hand() -// -// SpacerH10() -// -// Box(modifier = Modifier.fillMaxWidth()) { -// Text( -// modifier = Modifier.align(Alignment.Center), -// text = stringResource(id = R.string.swapping_permission_header), -// color = TangemTheme.colors.text.primary1, -// style = TangemTheme.typography.subtitle1, -// ) -// IconButton( -// modifier = Modifier.align(Alignment.CenterEnd), -// onClick = { isPermissionAlertShow = true }, -// ) { -// Icon( -// painter = painterResource(id = R.drawable.ic_question_24), -// contentDescription = null, -// ) -// } -// } -// -// SpacerH10() -// -// Text( -// text = stringResource( -// id = R.string.swapping_permission_subheader, -// data.currency, -// ), -// color = TangemTheme.colors.text.secondary, -// style = TangemTheme.typography.body2, -// textAlign = TextAlign.Center, -// modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing8), -// ) -// -// SpacerH16() -// -// ApprovalBottomSheetInfo(data) -// -// SpacerH28() -// -// PrimaryButtonIconEnd( -// text = stringResource(id = R.string.swapping_permission_buttons_approve), -// iconResId = R.drawable.ic_tangem_24, -// modifier = Modifier.fillMaxWidth(), -// onClick = data.approveButton.onClick, -// ) -// -// SpacerH12() -// -// SecondaryButton( -// text = stringResource(id = R.string.common_cancel), -// modifier = Modifier.fillMaxWidth(), -// onClick = { -// onCancel() -// }, -// ) -// -// SpacerH16() -// -// // region dialog -// if (isPermissionAlertShow) { -// BasicDialog( -// message = stringResource(id = R.string.swapping_approve_information_text), -// title = stringResource(id = R.string.swapping_approve_information_title), -// confirmButton = DialogButton { isPermissionAlertShow = false }, -// onDismissDialog = {}, -// ) -// } -// } -// } - -// @Composable -// private fun ApprovalBottomSheetInfo(data: SwapPermissionState.ReadyForRequest) { -// Column( -// modifier = Modifier -// .background(color = TangemTheme.colors.background.primary) -// .fillMaxWidth(), -// horizontalAlignment = Alignment.CenterHorizontally, -// ) { -// AmountItem( -// currency = data.currency, -// approveType = data.approveType, -// onChangeApproveType = data.onChangeApproveType, -// approveItems = data.approveItems, -// ) -// SubtitleItem( -// subtitle = stringResource(id = R.string.swapping_permission_policy_type_footer), -// modifier = Modifier.fillMaxWidth(), -// ) -// SpacerH24() -// DividerBottomSheet() -// FeeItem(fee = data.fee.resolveReference()) -// SubtitleItem( -// subtitle = stringResource(id = R.string.swapping_permission_fee_footer), -// modifier = Modifier.fillMaxWidth(), -// ) -// } -// } -// -// @Composable -// private fun DividerBottomSheet() { -// Divider( -// color = TangemTheme.colors.stroke.primary, -// thickness = TangemTheme.dimens.size0_5, -// ) -// } -// -// @Composable -// private fun InformationItem(subtitle: String, value: String) { -// Row( -// modifier = Modifier -// .fillMaxWidth() -// .padding(vertical = TangemTheme.dimens.spacing16), -// horizontalArrangement = Arrangement.SpaceBetween, -// verticalAlignment = Alignment.CenterVertically, -// ) { -// Text( -// text = subtitle, -// color = TangemTheme.colors.text.primary1, -// style = TangemTheme.typography.subtitle1, -// maxLines = 1, -// ) -// -// MiddleEllipsisText( -// text = value, -// color = TangemTheme.colors.text.tertiary, -// style = TangemTheme.typography.body2, -// modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), -// ) -// } -// } -// -// @Composable -// private fun AmountItem( -// currency: String, -// approveType: ApproveType, -// approveItems: ImmutableList, -// onChangeApproveType: (ApproveType) -> Unit, -// ) { -// var isExpandSelector by remember { -// mutableStateOf(false) -// } -// Row( -// modifier = Modifier -// .fillMaxWidth() -// .padding(vertical = TangemTheme.dimens.spacing16), -// horizontalArrangement = Arrangement.SpaceBetween, -// verticalAlignment = Alignment.CenterVertically, -// ) { -// Text( -// text = stringResource(id = R.string.swapping_permission_rows_amount, currency), -// color = TangemTheme.colors.text.primary1, -// style = TangemTheme.typography.subtitle1, -// maxLines = 1, -// ) -// Box { -// SelectorItem( -// getTitleForApproveType(approveType = approveType), -// ) { -// isExpandSelector = true -// } -// DropdownSelector( -// isExpanded = isExpandSelector, -// onDismiss = { isExpandSelector = false }, -// onItemClick = { approveType -> -// isExpandSelector = false -// onChangeApproveType.invoke(approveType) -// }, -// items = approveItems, -// ) -// } -// } -// } -// -// @Composable -// private fun SelectorItem(title: String, onClick: () -> Unit) { -// Row( -// modifier = Modifier.clickable { onClick() }, -// ) { -// Text( -// text = title, -// color = TangemTheme.colors.text.primary1, -// style = TangemTheme.typography.body1, -// maxLines = 1, -// ) -// Icon( -// painter = painterResource(id = R.drawable.ic_chevron_24), -// tint = TangemTheme.colors.icon.primary1, -// contentDescription = null, -// ) -// } -// } -// -// @Composable -// private fun DropdownSelector( -// isExpanded: Boolean, -// onDismiss: () -> Unit, -// onItemClick: (ApproveType) -> Unit, -// items: ImmutableList, -// ) { -// DropdownMenu( -// expanded = isExpanded, -// onDismissRequest = onDismiss, -// modifier = Modifier -// .wrapContentSize() -// .background(TangemTheme.colors.background.secondary), -// ) { -// items.forEach { item -> -// DropdownMenuItem( -// onClick = { -// onItemClick.invoke(item) -// }, -// ) { -// Text( -// text = getTitleForApproveType(approveType = item), -// color = TangemTheme.colors.text.primary1, -// style = TangemTheme.typography.body1, -// maxLines = 1, -// ) -// } -// } -// } -// } -// -// @Composable -// private fun FeeItem(fee: String) { -// InformationItem( -// subtitle = stringResource(id = R.string.send_fee_label), -// value = fee, -// ) -// } -// -// @Composable -// private fun SubtitleItem(subtitle: String, modifier: Modifier = Modifier) { -// Text( -// modifier = modifier, -// text = subtitle, -// color = TangemTheme.colors.text.secondary, -// style = TangemTheme.typography.body2, -// ) -// } -// -// @Composable -// private fun getTitleForApproveType(approveType: ApproveType): String = when (approveType) { -// ApproveType.LIMITED -> stringResource(id = R.string.swapping_permission_current_transaction) -// ApproveType.UNLIMITED -> stringResource(id = R.string.swapping_permission_unlimited) -// } -// -// // region preview -// -// @Preview -// @Composable -// private fun Preview_AgreementBottomSheet_InLightTheme() { -// TangemTheme(isDark = false) { -// SwapPermissionBottomSheetContent(data = previewData) {} -// } -// } -// -// @Preview -// @Composable -// private fun Preview_AgreementBottomSheet_InDarkTheme() { -// TangemTheme(isDark = true) { -// SwapPermissionBottomSheetContent(data = previewData) {} -// } -// } -// -// private val previewData = SwapPermissionState.ReadyForRequest( -// currency = "DAI", -// amount = "∞", -// walletAddress = "", -// spenderAddress = "", -// fee = TextReference.Str("2,14$"), -// approveType = ApproveType.UNLIMITED, -// approveButton = ApprovePermissionButton(true) {}, -// cancelButton = CancelPermissionButton(true), -// onChangeApproveType = { ApproveType.UNLIMITED }, -// ) - -//endregion preview \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index e1e6a80d73..82b05a7c86 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -33,6 +34,8 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) { + BackHandler(onBack = onBack) + Scaffold( modifier = Modifier .systemBarsPadding() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 631bdb18a8..62425f7111 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -18,15 +18,13 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor +import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* -import com.tangem.feature.swap.models.ApproveType -import com.tangem.feature.swap.models.SwapStateHolder -import com.tangem.feature.swap.models.UiActions -import com.tangem.feature.swap.models.toDomainApproveType +import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter @@ -140,7 +138,7 @@ internal class SwapViewModel @Inject constructor( uiState = uiState.copy( onSelectTokenClick = { router.openScreen(SwapNavScreen.SelectToken) - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened) + sendSelectTokenScreenOpenedEvent() }, onSuccess = { router.openScreen(SwapNavScreen.Success) @@ -148,9 +146,14 @@ internal class SwapViewModel @Inject constructor( ) } - @Suppress("UnusedPrivateMember") + private fun sendSelectTokenScreenOpenedEvent() { + val isAnyAvailableTokensTo = dataState.tokensDataState?.toGroup?.available?.isNotEmpty() ?: false + val isAnyAvailableTokensFrom = dataState.tokensDataState?.fromGroup?.available?.isNotEmpty() ?: false + val isAnyAvailableTokens = isAnyAvailableTokensTo || isAnyAvailableTokensFrom + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(availableTokens = isAnyAvailableTokens)) + } + private fun initTokens() { - // new flow viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { swapInteractor.getTokensDataState(initialCryptoCurrency) @@ -190,6 +193,7 @@ internal class SwapViewModel @Inject constructor( tokensDataState = state, ) if (selectedCurrency == null) { + analyticsEventHandler.send(SwapEvents.NoticeNoAvailableTokensToSwap) uiState = stateBuilder.createNoAvailableTokensToSwapState( uiStateHolder = uiState, fromToken = fromCurrencyStatus, @@ -317,6 +321,14 @@ internal class SwapViewModel @Inject constructor( isManyProviders = dataState.lastLoadedSwapStates.size > 1, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, ) + if (uiState.warnings.any { it is SwapWarning.UnableToCoverFeeWarning }) { + analyticsEventHandler.send( + SwapEvents.NoticeNotEnoughFee( + token = initialCryptoCurrency.symbol, + blockchain = fromToken.currency.network.name, + ), + ) + } } is SwapState.EmptyAmountState -> { uiState = stateBuilder.createQuotesEmptyAmountState( @@ -332,10 +344,21 @@ internal class SwapViewModel @Inject constructor( toToken = dataState.toCryptoCurrency, dataError = state.error, ) + sendErrorAnalyticsEvent(state.error, provider) } } } + private fun sendErrorAnalyticsEvent(error: DataError, provider: SwapProvider) { + analyticsEventHandler.send( + SwapEvents.NoticeProviderError( + token = initialCryptoCurrency.symbol, + provider = provider, + errorCode = error.code, + ), + ) + } + private fun updateLoadedQuotes(state: Map): Pair { val nonEmptyStates = state.filter { it.value !is SwapState.EmptyAmountState } val selectedSwapProvider = if (nonEmptyStates.isNotEmpty()) { @@ -438,15 +461,22 @@ internal class SwapViewModel @Inject constructor( if (txHash.isNotEmpty()) { swapRouter.openUrl(url) } + analyticsEventHandler.send( + event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol), + ) }, onStatusClick = { val txExternalUrl = it.txExternalUrl if (!txExternalUrl.isNullOrBlank()) { swapRouter.openUrl(txExternalUrl) + analyticsEventHandler.send( + event = SwapEvents.ButtonStatus(initialCryptoCurrency.symbol), + ) } }, ) - analyticsEventHandler.send(SwapEvents.SwapInProgressScreen) + sendSuccessEvent() + swapRouter.openScreen(SwapNavScreen.Success) } is TxState.UserCancelled -> { @@ -468,6 +498,22 @@ internal class SwapViewModel @Inject constructor( } } + private fun sendSuccessEvent() { + val provider = dataState.selectedProvider ?: return + val fee = dataState.selectedFee?.feeType ?: return + val sendToken = dataState.fromCryptoCurrency?.currency?.symbol ?: return + val toToken = dataState.toCryptoCurrency?.currency?.symbol ?: return + + analyticsEventHandler.send( + SwapEvents.SwapInProgressScreen( + provider = provider, + commission = fee, + sendToken = sendToken, + receiveToken = toToken, + ), + ) + } + private fun givePermissionsToSwap() { viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { @@ -563,9 +609,9 @@ internal class SwapViewModel @Inject constructor( it.currencyStatus.currency.id.value == id } } - analyticsEventHandler.send( - event = SwapEvents.SearchTokenClicked(currencySymbol = foundToken?.currencyStatus?.currency?.symbol), - ) + foundToken?.currencyStatus?.currency?.symbol?.let { + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = true, token = it)) + } if (foundToken != null) { val fromToken: CryptoCurrencyStatus @@ -702,6 +748,9 @@ internal class SwapViewModel @Inject constructor( if (bottomSheet != null && bottomSheet.isShow) { uiState = stateBuilder.dismissBottomSheet(uiState) } else { + if (swapRouter.currentScreen == SwapNavScreen.SelectToken) { + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(tokenChosen = false)) + } swapRouter.back() } onSearchEntered("") @@ -752,6 +801,7 @@ internal class SwapViewModel @Inject constructor( } }, onProviderClick = { providerId -> + analyticsEventHandler.send(SwapEvents.ProviderClicked) val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates() val pricesLowerBest = getPricesLowerBest(states) val unavailableProviders = getUnavailableProvidersFor(dataState.lastLoadedSwapStates) @@ -768,6 +818,7 @@ internal class SwapViewModel @Inject constructor( val swapState = dataState.lastLoadedSwapStates[provider] val fromToken = dataState.fromCryptoCurrency if (provider != null && swapState != null && fromToken != null) { + analyticsEventHandler.send(SwapEvents.ProviderChosen(provider)) uiState = stateBuilder.updateSelectedProvider(uiState, provider.providerId) setupLoadedState( provider = provider, From d80b167451a72851cc02afd31a4c644d802b3475 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 14:07:17 +0200 Subject: [PATCH 132/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index f5dbf2a745..b9aa6f95e5 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,6 +1,5 @@ package com.tangem.feature.swap.domain -import arrow.core.flatten import arrow.core.getOrElse import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType @@ -129,10 +128,14 @@ internal class SwapInteractorImpl @Inject constructor( tokenInfoForFilter(it).network == currency.network.backendId } - val availableCryptoCurrencies = filteredPairs.mapNotNull { pair -> - val statuses = findCryptoCurrencyStatusByLeastInfo(tokenInfoForAvailable(pair), cryptoCurrenciesList) - statuses.map { CryptoCurrencySwapInfo(it, pair.providers) } - }.flatten() + val availableCryptoCurrencies = cryptoCurrenciesList.mapNotNull { pair -> + val providers = findProvidersForPair(pair, filteredPairs, tokenInfoForAvailable) + if (providers != null) { + CryptoCurrencySwapInfo(pair, providers) + } else { + null + } + } val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies .map { it.currencyStatus } @@ -144,13 +147,20 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private fun findCryptoCurrencyStatusByLeastInfo( - leastTokenInfo: LeastTokenInfo, - cryptoCurrencyStatusesList: List, - ): List { - return cryptoCurrencyStatusesList.filter { - it.currency.network.backendId == leastTokenInfo.network && - it.currency.getContractAddress() == leastTokenInfo.contractAddress + private fun findProvidersForPair( + cryptoCurrencyStatuses: CryptoCurrencyStatus, + swapPairsLeastList: List, + tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo, + ): List? { + return swapPairsLeastList.firstNotNullOfOrNull { + val listTokenInfo = tokenInfoForAvailable(it) + if (cryptoCurrencyStatuses.currency.network.backendId == listTokenInfo.network && + cryptoCurrencyStatuses.currency.getContractAddress() == listTokenInfo.contractAddress + ) { + it.providers + } else { + null + } } } From 53d1d8b9a6e633bd67dd46d789288fdc997a8d19 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 15:08:05 +0300 Subject: [PATCH 133/139] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 10 +++++-- .../swap/models/states/ProviderState.kt | 18 +++++++++++- .../tangem/feature/swap/ui/ProviderItem.kt | 2 +- .../tangem/feature/swap/ui/StateBuilder.kt | 15 ++++++---- .../feature/swap/viewmodels/SwapViewModel.kt | 28 ++++++++----------- 5 files changed, 46 insertions(+), 27 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index b9aa6f95e5..38ff3ef4af 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -295,9 +295,13 @@ internal class SwapInteractorImpl @Inject constructor( ) val fromTokenAddress = getTokenAddress(fromToken.currency) - val isAllowedToSpend = quotes.dataModel?.allowanceContract?.let { - isAllowedToSpend(networkId, fromToken.currency, amount, it) - } ?: true + val isAllowedToSpend = if (quotes.dataModel != null) { + quotes.dataModel.allowanceContract?.let { + isAllowedToSpend(networkId, fromToken.currency, amount, it) + } ?: true + } else { + false + } if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index 63088433c5..84254a5716 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -25,7 +25,7 @@ sealed class ProviderState { val subtitle: TextReference, val selectionType: SelectionType, val additionalBadge: AdditionalBadge, - val percentLowerThenBest: Float?, + val percentLowerThenBest: Float = 0f, override val onProviderClick: (String) -> Unit, ) : ProviderState() @@ -48,4 +48,20 @@ sealed class ProviderState { enum class SelectionType { NONE, CLICK, SELECT } +} + +object ProviderPercentDiffComparator : Comparator { + override fun compare(o1: ProviderState, o2: ProviderState): Int { + if (o1 is ProviderState.Content && o2 !is ProviderState.Content) { + return 1 + } + if (o1 !is ProviderState.Content && o2 is ProviderState.Content) { + return -1 + } + return if (o1 is ProviderState.Content && o2 is ProviderState.Content) { + o1.percentLowerThenBest.compareTo(o2.percentLowerThenBest) + } else { + 0 + } + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 3dead8dfcd..e5fe4ef2c9 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -151,7 +151,7 @@ private fun ProviderContentState( maxLines = 1, ) } - if (state.percentLowerThenBest != null) { + if (state.percentLowerThenBest > 0f) { AnimatedContent(targetState = state.percentLowerThenBest, label = "") { Text( text = "-$it%", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index bb0f7e892c..094eac3c40 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -780,9 +780,11 @@ internal class StateBuilder( unavailableProviders: List, onDismiss: () -> Unit, ): SwapStateHolder { - val availableProvidersStates = providersStates.entries.mapNotNull { - it.convertToProviderBottomSheetState(pricesLowerBest, actions.onProviderSelect) - } + val availableProvidersStates = providersStates.entries + .mapNotNull { + it.convertToProviderBottomSheetState(pricesLowerBest, actions.onProviderSelect) + } + .sortedWith(ProviderPercentDiffComparator) val unavailableProviderStates = unavailableProviders.map { it.convertToUnavailableProviderState( alertText = resourceReference(R.string.express_provider_not_available), @@ -1024,7 +1026,7 @@ internal class StateBuilder( subtitle = stringReference(rateString), additionalBadge = badge, selectionType = selectionType, - percentLowerThenBest = null, + percentLowerThenBest = ZERO_PERCENT, onProviderClick = onProviderClick, ) } @@ -1053,7 +1055,7 @@ internal class StateBuilder( subtitle = stringReference(rateString), additionalBadge = additionalBadge, selectionType = selectionType, - percentLowerThenBest = pricesLowerBest[this], + percentLowerThenBest = pricesLowerBest[this] ?: ZERO_PERCENT, onProviderClick = onProviderClick, ) } @@ -1087,7 +1089,7 @@ internal class StateBuilder( selectionType = selectionType, subtitle = alertText, additionalBadge = ProviderState.AdditionalBadge.Empty, - percentLowerThenBest = null, + percentLowerThenBest = ZERO_PERCENT, onProviderClick = onProviderClick, ) } @@ -1128,5 +1130,6 @@ internal class StateBuilder( private const val HUNDRED_PERCENTS = 100 private const val UNKNOWN_AMOUNT_SIGN = "—" private const val MAX_DECIMALS_TO_SHOW = 8 + private const val ZERO_PERCENT = 0f } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 62425f7111..e37c581deb 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -42,6 +42,7 @@ import java.text.DecimalFormat import java.text.NumberFormat import java.util.Locale import javax.inject.Inject +import kotlin.math.absoluteValue import kotlin.properties.Delegates typealias SuccessLoadedSwapData = Map @@ -863,21 +864,17 @@ internal class SwapViewModel @Inject constructor( } private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map { - val rates = state.mapValues { - it.value.fromTokenInfo.amountFiat.divide( - it.value.toTokenInfo.amountFiat, - 2, - RoundingMode.HALF_UP, - ) - } - val bestRate = rates.minByOrNull { it.value } ?: return emptyMap() - return rates.mapNotNull { - if (it.key != bestRate.key) { - val percentDiff = bestRate.value - .divide(it.value, 2, RoundingMode.HALF_UP) - .multiply(BigDecimal(HUNDRED_PERCENT)) - .toFloat() - HUNDRED_PERCENT - percentDiff + val bestRateEntry = state.maxByOrNull { it.value.toTokenInfo.tokenAmount.value } ?: return emptyMap() + val bestRate = bestRateEntry.value.toTokenInfo.tokenAmount.value + val hundredPercent = BigDecimal("100") + return state.mapNotNull { + if (it.key != bestRateEntry.key) { + val amount = it.value.toTokenInfo.tokenAmount.value + val percentDiff = BigDecimal.ONE.minus( + amount.divide(bestRate, RoundingMode.HALF_UP) + .multiply(hundredPercent), + ) + percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat().absoluteValue } else { null } @@ -935,6 +932,5 @@ internal class SwapViewModel @Inject constructor( private const val INITIAL_AMOUNT = "" private const val UPDATE_DELAY = 10000L private const val DEBOUNCE_AMOUNT_DELAY = 1000L - private const val HUNDRED_PERCENT = 100 } } \ No newline at end of file From 3c24d7f6c81085157e589782aa90a6722b84e9b3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 15:41:45 +0300 Subject: [PATCH 134/139] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/models/UiActions.kt | 1 + .../java/com/tangem/feature/swap/ui/StateBuilder.kt | 11 +++++++++++ .../tangem/feature/swap/viewmodels/SwapViewModel.kt | 7 +++++++ 3 files changed, 19 insertions(+) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index a4fa64ab24..48a3d55f55 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -15,6 +15,7 @@ data class UiActions( val openPermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, // region new actions + val onRetryClick: () -> Unit, val onClickFee: () -> Unit, val onSelectFeeType: (TxFee) -> Unit, val onProviderClick: (String) -> Unit, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 094eac3c40..a1c6e1e09f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -429,6 +429,17 @@ internal class StateBuilder( iconResId = R.drawable.ic_alert_circle_24, ), ) + is DataError.UnknownError -> SwapWarning.GeneralWarning( + notificationConfig = NotificationConfig( + title = resourceReference(R.string.common_error), + subtitle = resourceReference(R.string.swapping_generic_error), + iconResId = R.drawable.img_attention_20, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = actions.onRetryClick, + ), + ), + ) else -> SwapWarning.GeneralWarning( notificationConfig = NotificationConfig( title = resourceReference(R.string.common_error), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index e37c581deb..51bc827a3b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -338,6 +338,9 @@ internal class SwapViewModel @Inject constructor( ) } is SwapState.SwapError -> { + if (state.error is DataError.UnknownError) { + singleTaskScheduler.cancelTask() + } uiState = stateBuilder.createQuotesErrorState( uiStateHolder = uiState, swapProvider = provider, @@ -422,6 +425,7 @@ internal class SwapViewModel @Inject constructor( } } + @Suppress("LongMethod") private fun onSwapClick() { singleTaskScheduler.cancelTask() uiState = stateBuilder.createSwapInProgressState(uiState) @@ -833,6 +837,9 @@ internal class SwapViewModel @Inject constructor( swapRouter.openTokenDetails(it.walletId, swapInteractor.getNativeToken(dataState.networkId)) } }, + onRetryClick = { + startLoadingQuotesFromLastState() + }, ) } From e2c5c7ec7545c1e32203d4098127c0232edbe528 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 17:02:28 +0300 Subject: [PATCH 135/139] Updated on 2026-08-14 --- .../com/tangem/datasource/di/NetworkModule.kt | 8 +++++++- .../assets/configs/feature_toggles_config.json | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 17 ++++++++++------- .../feature/swap/viewmodels/SwapViewModel.kt | 5 ++--- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 74791c9c0c..754936c7a5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.di import android.content.Context import com.squareup.moshi.Moshi +import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.promotion.PromotionApi @@ -33,10 +34,15 @@ class NetworkModule { @ApplicationContext context: Context, expressAuthProvider: ExpressAuthProvider, ): TangemExpressApi { + val url = if (BuildConfig.ENVIRONMENT == "dev") { + DEV_EXPRESS_BASE_URL + } else { + PROD_EXPRESS_BASE_URL + } return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(DEV_EXPRESS_BASE_URL) + .baseUrl(url) .client( OkHttpClient.Builder() .addHeaders(Express(expressAuthProvider)) diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 106d635c23..2b5c465b78 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -37,6 +37,6 @@ }, { "name": "WALLETS_SCROLLING_PREVIEW_ENABLED", - "version": "5.4.0" + "version": "undefined" } ] diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 38ff3ef4af..5a843b643a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -74,10 +74,13 @@ internal class SwapInteractorImpl @Inject constructor( val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase(selectedWallet.walletId) .getOrElse { emptyList() } - val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses.filter { - it.currency.network.backendId != currency.network.backendId || - it.currency.getContractAddress() != currency.getContractAddress() - } + val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses + .filter { + val currencyFilter = it.currency.network.backendId != currency.network.backendId || + it.currency.getContractAddress() != currency.getContractAddress() + val statusFilter = it.value is CryptoCurrencyStatus.Loaded + statusFilter && currencyFilter + } if (walletCurrencyStatusesExceptInitial.isEmpty()) { return TokensDataStateExpress( @@ -128,10 +131,10 @@ internal class SwapInteractorImpl @Inject constructor( tokenInfoForFilter(it).network == currency.network.backendId } - val availableCryptoCurrencies = cryptoCurrenciesList.mapNotNull { pair -> - val providers = findProvidersForPair(pair, filteredPairs, tokenInfoForAvailable) + val availableCryptoCurrencies = cryptoCurrenciesList.mapNotNull { cryptoCurrencyStatus -> + val providers = findProvidersForPair(cryptoCurrencyStatus, filteredPairs, tokenInfoForAvailable) if (providers != null) { - CryptoCurrencySwapInfo(pair, providers) + CryptoCurrencySwapInfo(cryptoCurrencyStatus, providers) } else { null } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 51bc827a3b..1485871552 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -878,9 +878,8 @@ internal class SwapViewModel @Inject constructor( if (it.key != bestRateEntry.key) { val amount = it.value.toTokenInfo.tokenAmount.value val percentDiff = BigDecimal.ONE.minus( - amount.divide(bestRate, RoundingMode.HALF_UP) - .multiply(hundredPercent), - ) + amount.divide(bestRate, RoundingMode.HALF_UP), + ).multiply(hundredPercent) percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat().absoluteValue } else { null From b134a5845a00504e24ec328c457437155b91daeb Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 15:17:29 +0100 Subject: [PATCH 136/139] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/common/DialogManager.kt | 11 ++++++----- .../redux/walletconnect/WalletConnectMiddleware.kt | 6 +++--- .../details/redux/walletconnect/WalletConnectState.kt | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 1579ca569f..11af89cb89 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -65,15 +65,16 @@ class DialogManager : StoreSubscriber { messageRes = R.string.wallet_connect_scanner_error_not_valid_card, context = context, ) - is WalletConnectDialog.AddNetwork -> + is WalletConnectDialog.AddNetwork -> { + val message = context.getString( + R.string.wallet_connect_error_missing_blockchains, + ) + state.dialog.networks.joinToString() SimpleAlertDialog.create( titleRes = R.string.wallet_connect_title, - message = context.getString( - R.string.wallet_connect_network_not_found_format, - state.dialog.network, - ), + message = message, context = context, ) + } is WalletConnectDialog.OpeningSessionRejected -> { SimpleAlertDialog.create( titleRes = R.string.wallet_connect_title, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 3b96ded384..3044d21544 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -278,7 +278,7 @@ class WalletConnectMiddleware { ).guard { store.dispatchOnMain( GlobalAction.ShowDialog( - WalletConnectDialog.AddNetwork(blockchain.fullName), + WalletConnectDialog.AddNetwork(listOf(blockchain.fullName)), ), ) return@launch @@ -325,7 +325,7 @@ class WalletConnectMiddleware { is WalletConnectError.ApprovalErrorAddNetwork -> { store.dispatchOnMain( GlobalAction.ShowDialog( - WalletConnectDialog.UnsupportedNetwork(action.error.networks), + WalletConnectDialog.AddNetwork(action.error.networks), ), ) } @@ -417,7 +417,7 @@ class WalletConnectMiddleware { store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session)) store.dispatchOnMain( GlobalAction.ShowDialog( - WalletConnectDialog.AddNetwork(blockchain.fullName), + WalletConnectDialog.AddNetwork(listOf(blockchain.fullName)), ), ) return diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 93f000c3d5..e9354c13a7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -92,7 +92,7 @@ sealed class WalletConnectDialog : StateDialog { data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog() object UnsupportedCard : WalletConnectDialog() data class UnsupportedNetwork(val networks: List? = null) : WalletConnectDialog() - data class AddNetwork(val network: String) : WalletConnectDialog() + data class AddNetwork(val networks: List) : WalletConnectDialog() object OpeningSessionRejected : WalletConnectDialog() object SessionTimeout : WalletConnectDialog() data class ApproveWcSession( From 2f2a5ae7d6ac129ef0803eaafc029df9c79dda11 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 15:31:15 +0100 Subject: [PATCH 137/139] Updated on 2026-08-14 --- .../common/analytics/events/AnalyticsParam.kt | 17 +++++++++++++++++ .../tap/common/analytics/events/Settings.kt | 5 +++++ .../ui/appsettings/AppSettingsViewModel.kt | 8 ++++++++ 3 files changed, 30 insertions(+) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 21dcd6c657..e3195c7e34 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -1,5 +1,6 @@ package com.tangem.tap.common.analytics.events +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.features.details.redux.SecurityOption sealed class AnalyticsParam { @@ -139,6 +140,22 @@ sealed class AnalyticsParam { object SeedImport : WalletCreationType(value = "Seed Import") } + sealed class AppTheme(val value: String) { + object System : AppTheme("System") + object Dark : AppTheme("Dark") + object Light : AppTheme("Light") + + companion object { + fun fromAppThemeMode(mode: AppThemeMode): AppTheme { + return when (mode) { + AppThemeMode.FORCE_DARK -> Dark + AppThemeMode.FORCE_LIGHT -> Light + AppThemeMode.FOLLOW_SYSTEM -> System + } + } + } + } + companion object Key { const val BLOCKCHAIN = "blockchain" const val TOKEN = "Token" diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index 772c85c8b1..b2123614eb 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -84,5 +84,10 @@ sealed class Settings( event = "Main Currency Changed", params = mapOf("Currency Type" to currencyType), ) + + class ThemeSwitched(theme: AnalyticsParam.AppTheme) : AppSettings( + event = "App Theme Switched", + params = mapOf("State" to theme.value), + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index b7a081674b..3bebecb90a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -4,10 +4,13 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.LifecycleCoroutineScope +import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain @@ -121,6 +124,11 @@ internal class AppSettingsViewModel( dialog = dialogsFactory.createThemeModeSelectorDialog( selectedModeIndex = selectedMode.ordinal, onSelect = { mode -> + Analytics.send( + event = Settings.AppSettings.ThemeSwitched( + theme = AnalyticsParam.AppTheme.fromAppThemeMode(mode), + ), + ) store.dispatchOnMain(DetailsAction.AppSettings.ChangeAppThemeMode(mode)) dismissDialog() }, From 56c32291f290d0f7073fae00dd302ea1a8d11907 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Dec 2023 18:03:55 +0300 Subject: [PATCH 138/139] Updated on 2026-08-14 --- .../tangem/feature/swap/ui/StateBuilder.kt | 43 ++++++++++--------- .../feature/swap/viewmodels/SwapViewModel.kt | 5 +-- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index a1c6e1e09f..74a81982cb 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -20,6 +20,7 @@ import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.viewmodels.SwapProcessDataState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @@ -638,38 +639,40 @@ internal class StateBuilder( @Suppress("LongParameterList") fun createSuccessState( uiState: SwapStateHolder, - txState: TxState.TxSent, - fromAmount: BigDecimal, - toAmount: BigDecimal, + timeStamp: Long, txUrl: String, + dataState: SwapProcessDataState, onExploreClick: () -> Unit, onStatusClick: () -> Unit, ): SwapStateHolder { + val fee = requireNotNull(dataState.selectedFee) + val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) + val toCryptoCurrency = requireNotNull(dataState.toCryptoCurrency) + val fromAmount = requireNotNull(dataState.amount?.toBigDecimal()) + val toAmount = requireNotNull(dataState.swapDataModel?.toTokenAmount?.value) val providerState = uiState.providerState as ProviderState.Content - val fromToken = requireNotNull((uiState.sendCardData as? SwapCardState.SwapCardData)?.token) - val toToken = requireNotNull((uiState.receiveCardData as? SwapCardState.SwapCardData)?.token) - val fromTokenIconState = iconStateConverter.convert(fromToken) - val toTokenIconState = iconStateConverter.convert(toToken) - val fee = uiState.fee as? FeeItemState.Content ?: return uiState - val fromFiatAmount = getFormattedFiatAmount(fromToken.value.fiatRate?.multiply(fromAmount)) - val toFiatAmount = getFormattedFiatAmount(toToken.value.fiatRate?.multiply(toAmount)) + + val fromCryptoAmount = BigDecimalFormatter.formatCryptoAmount(fromAmount, fromCryptoCurrency.currency) + val toCryptoAmount = BigDecimalFormatter.formatCryptoAmount(toAmount, toCryptoCurrency.currency) + val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount)) + val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount)) return uiState.copy( successState = SwapSuccessStateHolder( - timestamp = txState.timestamp, + timestamp = timeStamp, txUrl = txUrl, - providerName = TextReference.Str(providerState.name), - providerType = TextReference.Str(providerState.type), + providerName = stringReference(providerState.name), + providerType = stringReference(providerState.type), showStatusButton = providerState.type == ExchangeProviderType.CEX.name, providerIcon = providerState.iconUrl, - fee = TextReference.Str("${fee.amountCrypto} ${fee.symbolCrypto} (${fee.amountFiatFormatted})"), rate = providerState.subtitle, - fromTokenAmount = TextReference.Str(txState.fromAmount.orEmpty()), - toTokenAmount = TextReference.Str(txState.toAmount.orEmpty()), - fromTokenFiatAmount = TextReference.Str(fromFiatAmount), - toTokenFiatAmount = TextReference.Str(toFiatAmount), - fromTokenIconState = fromTokenIconState, - toTokenIconState = toTokenIconState, + fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"), + fromTokenAmount = stringReference(fromCryptoAmount), + toTokenAmount = stringReference(toCryptoAmount), + fromTokenFiatAmount = stringReference(fromFiatAmount), + toTokenFiatAmount = stringReference(toFiatAmount), + fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency), + toTokenIconState = iconStateConverter.convert(toCryptoCurrency), onExploreButtonClick = onExploreClick, onStatusButtonClick = onStatusClick, ), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 1485871552..b2ba4e8a24 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -457,9 +457,8 @@ internal class SwapViewModel @Inject constructor( ) uiState = stateBuilder.createSuccessState( uiState = uiState, - txState = it, - fromAmount = dataState.amount?.toBigDecimal() ?: BigDecimal.ZERO, - toAmount = dataState.swapDataModel?.toTokenAmount?.value ?: BigDecimal.ZERO, + timeStamp = it.timestamp, + dataState = dataState, txUrl = url, onExploreClick = { val txHash = it.txAddress From 0d6be14aa3df07977652adc8df3f70e2e701cdde Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Dec 2023 14:04:43 +0300 Subject: [PATCH 139/139] Updated on 2026-08-14 --- gradle/dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 6b23060307..edc76744e7 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,9 +88,9 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-411" +tangemBlockchainSdk = "release-app_5.4-412" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-312" +tangemCardSdk = "release-app_5.4-315" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem