Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-30 14:23:50 +03:00
commit c32a5fa18d
711 changed files with 18558 additions and 4312 deletions

View file

@ -40,6 +40,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.nft.models)
implementation(projects.domain.walletConnect.models)
implementation(projects.domain.yieldSupply.models)
/** Tangem libraries */
implementation(tangemDeps.blockchain)

View file

@ -1,9 +1,11 @@
package com.tangem.datasource.api.common.blockaid
import com.tangem.datasource.api.common.blockaid.models.request.DomainScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionBulkScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest
import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse
import com.tangem.datasource.api.common.blockaid.models.response.GasEstimationResponse
import com.tangem.datasource.api.common.blockaid.models.response.SolanaTransactionResponse
import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse
import retrofit2.http.Body
@ -19,4 +21,7 @@ interface BlockAidApi {
@POST("solana/message/scan")
suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): SolanaTransactionResponse
@POST("evm/transaction-bulk/scan")
suspend fun scanEvmTransactionBulk(@Body request: EvmTransactionBulkScanRequest): List<GasEstimationResponse>
}

View file

@ -0,0 +1,7 @@
package com.tangem.datasource.api.common.blockaid.models.request
enum class BlockAidScanOptions(val value: String) {
Simulation("simulation"),
Validation("validation"),
GasEstimation("gas_estimation"),
}

View file

@ -14,9 +14,25 @@ data class EvmTransactionScanRequest(
@Json(name = "metadata") val metadata: TransactionMetadata,
)
@JsonClass(generateAdapter = true)
data class EvmTransactionBulkScanRequest(
@Json(name = "chain") val chain: String,
@Json(name = "options") val options: List<String>,
@Json(name = "metadata") val metadata: TransactionMetadata,
@Json(name = "data") val data: List<Data>,
@Json(name = "aggregated") val aggregated: Boolean = false,
)
@JsonClass(generateAdapter = true)
data class RpcData(
@Json(name = "jsonrpc") val jsonrpc: String = "2.0",
@Json(name = "method") val method: String,
@Json(name = "params") val params: List<Map<String, String>>,
)
@JsonClass(generateAdapter = true)
data class Data(
@Json(name = "from") val from: String,
@Json(name = "to") val to: String,
@Json(name = "data") val data: String,
)

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.common.blockaid.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GasEstimationResponse(
@Json(name = "gas_estimation") val gasEstimation: GasEstimationItem,
)
@JsonClass(generateAdapter = true)
data class GasEstimationItem(
@Json(name = "estimate") val estimate: String,
)

View file

@ -26,6 +26,7 @@ sealed class ApiConfig {
StakeKit,
TangemPay,
BlockAid,
YieldSupply,
}
private fun initializeId(): ID {
@ -35,6 +36,7 @@ sealed class ApiConfig {
is StakeKit -> ID.StakeKit
is TangemPay -> ID.TangemPay
is BlockAid -> ID.BlockAid
is YieldSupply -> ID.YieldSupply
}
}

View file

@ -2,12 +2,15 @@ package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/** TangemTech [ApiConfig] */
internal class TangemTech(
private val environmentConfigStorage: EnvironmentConfigStorage,
private val appVersionProvider: AppVersionProvider,
private val authProvider: AuthProvider,
private val appInfoProvider: AppInfoProvider,
@ -38,29 +41,41 @@ internal class TangemTech(
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
headers = createHeaders(ApiEnvironment.DEV),
)
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
headers = createHeaders(ApiEnvironment.STAGE),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
headers = createHeaders(ApiEnvironment.MOCK),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.tangem.org/",
headers = createHeaders(),
headers = createHeaders(ApiEnvironment.PROD),
)
private fun createHeaders() = buildMap {
private fun createHeaders(apiEnvironment: ApiEnvironment) = buildMap {
put(key = "api-key", value = ProviderSuspend { getApiKey(apiEnvironment) })
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)
}
private fun getApiKey(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.MOCK,
ApiEnvironment.DEV,
ApiEnvironment.DEV_2,
-> environmentConfigStorage.getConfigSync().tangemApiKeyDev
ApiEnvironment.STAGE -> environmentConfigStorage.getConfigSync().tangemApiKeyStage
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey
} ?: error("No tangem tech api config provided")
}
}

View file

@ -0,0 +1,81 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/** YieldSupply [ApiConfig] */
internal class YieldSupply(
private val environmentConfigStorage: EnvironmentConfigStorage,
private val appVersionProvider: AppVersionProvider,
private val authProvider: AuthProvider,
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs = listOf(
createDevEnvironment(),
createStageEnvironment(),
createMockedEnvironment(),
createProdEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
-> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.DEV,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(ApiEnvironment.DEV),
)
private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.STAGE,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(ApiEnvironment.STAGE),
)
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(ApiEnvironment.MOCK),
)
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://yield.tangem.org/",
headers = createHeaders(ApiEnvironment.PROD),
)
private fun createHeaders(apiEnvironment: ApiEnvironment) = buildMap {
put(key = "api-key", value = ProviderSuspend { getApiKey(apiEnvironment) })
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)
}
private fun getApiKey(apiEnvironment: ApiEnvironment): String {
return when (apiEnvironment) {
ApiEnvironment.MOCK,
ApiEnvironment.DEV,
ApiEnvironment.DEV_2,
-> environmentConfigStorage.getConfigSync().tangemApiKeyDev
ApiEnvironment.STAGE -> environmentConfigStorage.getConfigSync().tangemApiKeyStage
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey
} ?: error("No tangem tech api config provided")
}
}

View file

@ -26,10 +26,10 @@ internal class DevApiConfigsManager(
) : MutableApiConfigsManager() {
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
field = MutableStateFlow(value = getInitialConfigs())
field = MutableStateFlow(value = getInitialConfigs())
override val isInitialized: StateFlow<Boolean>
field = MutableStateFlow(value = false)
field = MutableStateFlow(value = false)
override fun initialize() {
isInitialized.value = false

View file

@ -22,7 +22,7 @@ internal class MockApiConfigsManager(
) : MutableApiConfigsManager() {
override val configs: StateFlow<Map<ApiConfig, ApiEnvironment>>
field = MutableStateFlow(value = getInitialConfigs())
field = MutableStateFlow(value = getInitialConfigs())
override val isInitialized: StateFlow<Boolean> = MutableStateFlow(value = true)

View file

@ -20,7 +20,7 @@ abstract class MutableApiConfigsManager : ApiConfigsManager {
* These listeners are notified whenever an environment change occurs.
*/
protected val registerListeners: Set<ApiConfigEnvChangeListener>
field = mutableSetOf<ApiConfigEnvChangeListener>()
field = mutableSetOf<ApiConfigEnvChangeListener>()
/** Change api environment [environment] by [id] */
abstract suspend fun changeEnvironment(id: String, environment: ApiEnvironment)

View file

@ -7,8 +7,11 @@ import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
private const val TX_HISTORY_PAGING_DEFAULT_LIMIT = 20
@Suppress("TooManyFunctions")
interface TangemPayApi {
@ -107,6 +110,13 @@ interface TangemPayApi {
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
@GET("v1/customer/transactions")
suspend fun getTangemPayTxHistory(
@Header("Authorization") authHeader: String,
@Query("cursor") cursor: String?,
@Query("limit") limit: Int = TX_HISTORY_PAGING_DEFAULT_LIMIT,
): ApiResponse<TangemPayTxHistoryResponse>
@GET("v1/customer/kyc")
suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse<KycAccessInfoResponse>
@ -115,4 +125,16 @@ interface TangemPayApi {
@POST("v1/deeplink/validate")
suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse<DeeplinkValidityResponse>
@GET("v1/order/{order_id}")
suspend fun getOrder(
@Header("Authorization") authHeader: String,
@Path("order_id") orderId: String,
): ApiResponse<OrderResponse>
@POST("v1/order")
suspend fun createOrder(
@Header("Authorization") authHeader: String,
@Body body: OrderRequest,
): ApiResponse<OrderResponse>
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class OrderRequest(
@Json(name = "wallet_address") val walletAddress: String,
)

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class CustomerMeResponse(
@ -16,6 +17,8 @@ data class CustomerMeResponse(
@Json(name = "product_instance") val productInstance: ProductInstance?,
@Json(name = "payment_account") val paymentAccount: PaymentAccount?,
@Json(name = "kyc") val kyc: Kyc?,
@Json(name = "card") val card: Card?,
@Json(name = "balance") val balance: Balance?,
)
@JsonClass(generateAdapter = true)
@ -45,4 +48,25 @@ data class CustomerMeResponse(
@Json(name = "review_answer") val reviewAnswer: String,
@Json(name = "created_at") val createdAt: String,
)
@JsonClass(generateAdapter = true)
data class Card(
@Json(name = "token") val token: String,
@Json(name = "expiration_month") val expirationMonth: Int,
@Json(name = "expiration_year") val expirationYear: Int,
@Json(name = "emboss_name") val embossName: String,
@Json(name = "card_type") val cardType: String,
@Json(name = "card_status") val cardStatus: String,
@Json(name = "card_number_end") val cardNumberEnd: String,
)
@JsonClass(generateAdapter = true)
data class Balance(
@Json(name = "currency") val currency: String,
@Json(name = "available_balance") val availableBalance: BigDecimal,
@Json(name = "credit_limit") val creditLimit: BigDecimal,
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
@Json(name = "posted_charges") val postedCharges: BigDecimal,
@Json(name = "balance_due") val balanceDue: BigDecimal,
)
}

View file

@ -0,0 +1,33 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class OrderResponse(
@Json(name = "result") val result: Result?,
@Json(name = "error") val error: String?,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "id") val id: String,
@Json(name = "customer_id") val customerId: String?,
@Json(name = "type") val type: String?,
@Json(name = "status") val status: String,
@Json(name = "step") val step: String?,
@Json(name = "data") val data: Data,
@Json(name = "step_change_code") val stepChangeCode: Int?,
@Json(name = "created_at") val createdAt: String?,
@Json(name = "updated_at") val updatedAt: String?,
) {
@JsonClass(generateAdapter = true)
data class Data(
@Json(name = "type") val type: String?,
@Json(name = "specification_name") val specificationName: String?,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "emboss_name") val embossName: String?,
@Json(name = "product_instance_id") val productInstanceId: String?,
@Json(name = "payment_account_id") val paymentAccountId: String?,
)
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.datasource.api.pay.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class TangemPayTxHistoryResponse(
@Json(name = "error") val error: String?,
@Json(name = "result") val result: Result,
) {
@JsonClass(generateAdapter = true)
data class Result(
@Json(name = "transactions") val transactions: List<Transaction>,
)
@JsonClass(generateAdapter = true)
data class Transaction(
@Json(name = "id") val id: String, // UUID (used as cursor for pagination)
@Json(name = "type") val type: String, // "SPEND", "COLLATERAL", "PAYMENT", "FEE"
@Json(name = "spend") val spend: Spend? = null,
@Json(name = "collateral") val collateral: Collateral? = null,
@Json(name = "payment") val payment: Payment? = null,
@Json(name = "fee") val fee: Fee? = null,
)
@JsonClass(generateAdapter = true)
data class Spend(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "local_amount") val localAmount: BigDecimal? = null,
@Json(name = "local_currency") val localCurrency: String? = null,
@Json(name = "authorized_amount") val authorizedAmount: BigDecimal? = null,
@Json(name = "authorization_method") val authorizationMethod: String? = null,
@Json(name = "memo") val memo: String? = null,
@Json(name = "receipt") val receipt: Boolean? = null,
@Json(name = "merchant_name") val merchantName: String? = null,
@Json(name = "merchant_category") val merchantCategory: String? = null,
@Json(name = "merchant_category_code") val merchantCategoryCode: String? = null,
@Json(name = "merchant_id") val merchantId: String? = null,
@Json(name = "enriched_merchant_icon") val enrichedMerchantIcon: String? = null,
@Json(name = "enriched_merchant_name") val enrichedMerchantName: String? = null,
@Json(name = "enriched_merchant_category") val enrichedMerchantCategory: String? = null,
@Json(name = "card_id") val cardId: String? = null,
@Json(name = "card_type") val cardType: String? = null,
@Json(name = "status") val status: String? = null,
@Json(name = "declined_reason") val declinedReason: String? = null,
@Json(name = "authorized_at") val authorizedAt: DateTime? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
@JsonClass(generateAdapter = true)
data class Collateral(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "memo") val memo: String? = null,
@Json(name = "chain_id") val chainId: Long? = null,
@Json(name = "wallet_address") val walletAddress: String? = null,
@Json(name = "transaction_hash") val transactionHash: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
@JsonClass(generateAdapter = true)
data class Payment(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "memo") val memo: String? = null,
@Json(name = "chain_id") val chainId: Long? = null,
@Json(name = "wallet_address") val walletAddress: String? = null,
@Json(name = "transaction_hash") val transactionHash: String? = null,
@Json(name = "status") val status: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
@JsonClass(generateAdapter = true)
data class Fee(
@Json(name = "amount") val amount: BigDecimal,
@Json(name = "currency") val currency: String,
@Json(name = "description") val description: String? = null,
@Json(name = "posted_at") val postedAt: DateTime? = null,
)
}

View file

@ -198,6 +198,7 @@ data class YieldDTO(
enum class RewardTypeDTO {
@Json(name = "apy")
APY, // compound rate
@Json(name = "apr")
APR, // simple rate,

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
import com.tangem.datasource.api.tangemTech.models.YieldTokenStatusResponse
import retrofit2.http.GET
import retrofit2.http.Path
interface YieldSupplyApi {
@GET("v1/yield/markets")
suspend fun getYieldMarkets(): ApiResponse<YieldMarketsResponse>
@GET("v1/yield/token/{tokenAddress}")
suspend fun getYieldTokenStatus(@Path("tokenAddress") tokenAddress: String): ApiResponse<YieldTokenStatusResponse>
}

View file

@ -24,7 +24,5 @@ data class SeedPhraseNotificationDTO(val status: Status) {
@Json(name = "accepted")
ACCEPTED,
;
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldMarketsResponse(
@Json(name = "tokens") val marketDtos: List<MarketDto>,
@Json(name = "lastUpdatedAt") val lastUpdated: String,
) {
@JsonClass(generateAdapter = true)
data class MarketDto(
@Json(name = "tokenAddress") val tokenAddress: String,
@Json(name = "tokenSymbol") val tokenSymbol: String,
@Json(name = "tokenName") val tokenName: String,
@Json(name = "apy") val apy: BigDecimal,
@Json(name = "totalSupplied") val totalSupplied: String,
@Json(name = "totalBorrowed") val totalBorrowed: String,
@Json(name = "liquidityRate") val liquidityRate: String,
@Json(name = "borrowRate") val borrowRate: String,
@Json(name = "utilizationRate") val utilizationRate: BigDecimal,
@Json(name = "isActive") val isActive: Boolean,
@Json(name = "ltv") val ltv: BigDecimal,
@Json(name = "liquidationThreshold") val liquidationThreshold: BigDecimal,
@Json(name = "decimals") val decimals: Int,
)
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldTokenStatusResponse(
@Json(name = "tokenAddress") val tokenAddress: String,
@Json(name = "tokenSymbol") val tokenSymbol: String,
@Json(name = "userBalance") val userBalance: String,
@Json(name = "earnedYield") val earnedYield: String,
@Json(name = "currentApy") val currentApy: BigDecimal,
@Json(name = "totalDeposited") val totalDeposited: String,
@Json(name = "moduleAddress") val moduleAddress: String,
@Json(name = "status") val status: String,
@Json(name = "lastUpdateAt") val lastUpdateAt: String,
)

View file

@ -42,10 +42,12 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideTangemTechConfig(
environmentConfigStorage: EnvironmentConfigStorage,
appVersionProvider: AppVersionProvider,
authProvider: AuthProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig = TangemTech(
environmentConfigStorage = environmentConfigStorage,
appVersionProvider = appVersionProvider,
authProvider = authProvider,
appInfoProvider = appInfoProvider,

View file

@ -15,6 +15,7 @@ import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
import com.tangem.datasource.di.utils.RetrofitApiBuilder
import com.tangem.datasource.di.utils.RetrofitApiBuilder.Timeouts
import com.tangem.datasource.local.preferences.AppPreferencesStore
@ -88,6 +89,15 @@ internal object NetworkModule {
)
}
@Provides
@Singleton
fun provideYieldSupplyApi(retrofitApiBuilder: RetrofitApiBuilder): YieldSupplyApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.YieldSupply,
applyTimeoutAnnotations = true,
)
}
@Provides
@Singleton
fun provideTangemTechMarketsApi(retrofitApiBuilder: RetrofitApiBuilder): TangemTechMarketsApi {

View file

@ -3,6 +3,8 @@ package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.txhistory.DefaultTxHistoryItemsStore
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
import com.tangem.datasource.local.visa.DefaultTangemPayTxHistoryItemsStore
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -20,4 +22,12 @@ internal object TxHistoryItemsStoreModule {
dataStore = RuntimeDataStore(),
)
}
@Provides
@Singleton
fun provideTangemPayTxHistoryItemsStore(): TangemPayTxHistoryItemsStore {
return DefaultTangemPayTxHistoryItemsStore(
dataStore = RuntimeDataStore(),
)
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.datasource.di
import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.listTypes
import com.tangem.domain.yield.supply.models.YieldMarketToken
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object YieldSupplyModule {
@Provides
@Singleton
fun provideYieldMarketsStore(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): YieldMarketsStore {
return DefaultYieldMarketsStore(
persistenceStore = DataStoreFactory.create(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = listTypes<YieldMarketToken>(),
defaultValue = emptyList(),
),
produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
),
)
}
}

View file

@ -17,4 +17,7 @@ data class EnvironmentConfig(
val devExpress: ExpressModel? = null,
val stakeKitApiKey: String? = null,
val blockAidApiKey: String? = null,
val tangemApiKey: String? = null,
val tangemApiKeyDev: String? = null,
val tangemApiKeyStage: String? = null,
)

View file

@ -26,6 +26,9 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
devExpress = value.devExpress,
stakeKitApiKey = value.stakeKitApiKey,
blockAidApiKey = value.blockaidApiKey,
tangemApiKey = value.tangemApiKey,
tangemApiKeyDev = value.tangemApiKeyDev,
tangemApiKeyStage = value.tangemApiKeyStage,
)
}
}

View file

@ -40,6 +40,9 @@ class EnvironmentConfigModel(
@Json(name = "moralisApiKey") val moralisApiKey: String?,
@Json(name = "nftScanApiKey") val nftScanApiKey: String?,
@Json(name = "blockaidApiKey") val blockaidApiKey: String?,
@Json(name = "tangemApiKey") val tangemApiKey: String?,
@Json(name = "tangemApiKeyDev") val tangemApiKeyDev: String?,
@Json(name = "tangemApiKeyStage") val tangemApiKeyStage: String?,
@Json(name = "etherscanApiKey") val etherScanApiKey: String?,
)

View file

@ -129,6 +129,8 @@ object PreferencesKeys {
val WALLETS_NFT_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "walletsNftEnabledStates") }
val YIELD_SUPPLY_WARNINGS_STATES_KEY by lazy { stringPreferencesKey(name = "yieldSupplyWarningsStates") }
// region Notifications
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.local.visa
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
internal class DefaultTangemPayTxHistoryItemsStore(
dataStore: StringKeyDataStore<Map<String, List<TangemPayTxHistoryItem>>>,
) : TangemPayTxHistoryItemsStore,
StringKeyDataStoreDecorator<String, Map<String, List<TangemPayTxHistoryItem>>>(dataStore) {
override fun provideStringKey(key: String): String = key
override suspend fun getSyncOrNull(key: String, cursor: String): List<TangemPayTxHistoryItem>? {
val storedValue = getSyncOrNull(key)
return storedValue?.get(cursor)
}
override suspend fun store(key: String, cursor: String, value: List<TangemPayTxHistoryItem>) {
val oldValue = getSyncOrNull(key).orEmpty()
val newValue = oldValue.toMutableMap().apply { put(cursor, value) }
store(key, newValue)
}
}

View file

@ -8,9 +8,9 @@ interface TangemPayStorage {
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
suspend fun storeCustomerWalletAddress(customerWalletAddress: String)
suspend fun storeOrderId(customerWalletAddress: String, orderId: String)
suspend fun getCustomerWalletAddress(): String?
suspend fun getOrderId(customerWalletAddress: String): String?
suspend fun clear(customerWalletAddress: String)
}

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.local.visa
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
interface TangemPayTxHistoryItemsStore {
suspend fun getSyncOrNull(key: String, cursor: String): List<TangemPayTxHistoryItem>?
suspend fun remove(key: String)
suspend fun store(key: String, cursor: String, value: List<TangemPayTxHistoryItem>)
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.yieldsupply
import androidx.datastore.core.DataStore
import com.tangem.domain.yield.supply.models.YieldMarketToken
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
internal class DefaultYieldMarketsStore(
private val persistenceStore: DataStore<List<YieldMarketToken>>,
) : YieldMarketsStore {
override fun get(): Flow<List<YieldMarketToken>> = persistenceStore.data
override suspend fun getSyncOrNull(): List<YieldMarketToken>? {
return persistenceStore.data.firstOrNull()
}
override suspend fun store(items: List<YieldMarketToken>) {
persistenceStore.updateData { _ -> items }
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.datasource.local.yieldsupply
import com.tangem.domain.yield.supply.models.YieldMarketToken
import kotlinx.coroutines.flow.Flow
interface YieldMarketsStore {
fun get(): Flow<List<YieldMarketToken>>
suspend fun getSyncOrNull(): List<YieldMarketToken>?
suspend fun store(items: List<YieldMarketToken>)
}

View file

@ -39,6 +39,7 @@ class ApiConfigTest {
}
ApiConfig.ID.TangemTech -> {
TangemTech(
environmentConfigStorage = mockk(),
appVersionProvider = mockk(),
authProvider = mockk(),
appInfoProvider = mockk(),

View file

@ -86,6 +86,7 @@ internal class ProdApiConfigsManagerTest {
}
ApiConfig.ID.TangemTech -> {
TangemTech(
environmentConfigStorage = environmentConfigStorage,
appVersionProvider = appVersionProvider,
authProvider = appAuthProvider,
appInfoProvider = appInfoProvider,

View file

@ -0,0 +1,92 @@
package com.tangem.pagination.fetcher
import com.tangem.pagination.BatchFetchResult
import com.tangem.pagination.exception.EndOfPaginationException
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
/**
* First page: cursor = null
* Next pages: cursor = cursorFromItem(lastItemOfPreviousPage)
*/
class CursorBatchFetcher<TRequestParams : Any, TItem : Any>(
private val prefetchDistance: Int,
private val batchSize: Int,
private val subFetcher: SubFetcher<TRequestParams, TItem>,
private val cursorFromItem: (TItem) -> String,
) : BatchFetcher<TRequestParams, List<TItem>> {
data class Request<TRequestParams>(
val limit: Int,
val cursor: String?,
val params: TRequestParams,
)
fun interface SubFetcher<TRequestParams : Any, TItem : Any> {
suspend fun fetch(
request: Request<TRequestParams>,
lastResult: BatchFetchResult<List<TItem>>?,
isFirstBatchFetching: Boolean,
): BatchFetchResult<List<TItem>>
}
private val lastRequest = MutableStateFlow<Request<TRequestParams>?>(null)
override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult<List<TItem>> {
val request = Request(
cursor = null,
limit = prefetchDistance,
params = requestParams,
)
val result = runCatching {
subFetcher.fetch(request = request, lastResult = null, isFirstBatchFetching = true)
}.getOrElse {
currentCoroutineContext().ensureActive()
return BatchFetchResult.Error(it)
}
lastRequest.value = request
return result
}
override suspend fun fetchNext(
overrideRequestParams: TRequestParams?,
lastResult: BatchFetchResult<List<TItem>>,
): BatchFetchResult<List<TItem>> {
val lastRequest = requireNotNull(lastRequest.value) { "fetchFirst() must be called before fetchNext()" }
if (lastResult is BatchFetchResult.Success && lastResult.last && overrideRequestParams == null) {
return BatchFetchResult.Error(EndOfPaginationException())
}
val nextReq: Request<TRequestParams> =
if (lastResult is BatchFetchResult.Success<List<TItem>>) {
val items = lastResult.data
if (items.isEmpty()) {
return BatchFetchResult.Error(EndOfPaginationException())
}
val nextCursor = cursorFromItem(items.last())
Request(
cursor = nextCursor,
limit = batchSize,
params = overrideRequestParams ?: lastRequest.params,
)
} else {
lastRequest.copy(limit = batchSize, params = overrideRequestParams ?: lastRequest.params)
}
val result = runCatching {
subFetcher.fetch(request = nextReq, lastResult = lastResult, isFirstBatchFetching = false)
}.getOrElse {
currentCoroutineContext().ensureActive()
return BatchFetchResult.Error(it)
}
this.lastRequest.value = nextReq
return result
}
}

View file

@ -1740,7 +1740,6 @@
<string name="yield_module_earn_sheet_total_earnings_title">Total earnings</string>
<string name="yield_module_earn_sheet_transfers_title">Transfers to Aave</string>
<string name="yield_module_explore_sheet_explore_aave_button_title">Explore Aave</string>
<string name="yield_module_explore_sheet_title">Your %s is deposited in Aave </string>
<string name="yield_module_fee_policy_sheet_current_fee_note">This is the current supply fee on %s. The live cost will be shown on the Receive Screen.</string>
<string name="yield_module_fee_policy_sheet_current_fee_title">Current fee</string>
<string name="yield_module_fee_policy_sheet_description">All future %s top-ups will be supplied to Aave automatically, with the transaction fee deducted.</string>

View file

@ -68,9 +68,9 @@ fun CardWithIcon(
internal fun IconWithTitleAndDescription(
title: String,
description: String?,
iconBackground: Color = TangemTheme.colors.background.secondary,
icon: @Composable () -> Unit,
additionalContent: @Composable () -> Unit = {},
iconBackground: Color = TangemTheme.colors.background.secondary,
) {
Row(
modifier = Modifier

View file

@ -0,0 +1,94 @@
package com.tangem.core.ui.components
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.res.LocalIsInDarkTheme
@Composable
fun DialogFullScreen(
onDismissRequest: () -> Unit,
properties: DialogProperties = DialogProperties(),
content: @Composable () -> Unit,
) {
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
dismissOnBackPress = properties.dismissOnBackPress,
dismissOnClickOutside = properties.dismissOnClickOutside,
securePolicy = properties.securePolicy,
usePlatformDefaultWidth = true, // must be true as a part of work around
decorFitsSystemWindows = false,
),
content = {
val activityWindow = getActivityWindow()
val dialogWindow = getDialogWindow()
val parentView = LocalView.current.parent as View
SideEffect {
if (activityWindow != null && dialogWindow != null) {
val attributes = WindowManager.LayoutParams().apply {
copyFrom(activityWindow.attributes)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
} else {
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
type = dialogWindow.attributes.type
}
dialogWindow.attributes = attributes
parentView.layoutParams =
FrameLayout.LayoutParams(
activityWindow.decorView.width,
activityWindow.decorView.height,
)
}
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
val systemUiController = rememberSystemUiController(getActivityWindow())
val dialogSystemUiController = rememberSystemUiController(getDialogWindow())
SideEffect {
systemUiController.setSystemBarsColor(color = Color.Transparent)
dialogSystemUiController.setSystemBarsColor(color = Color.Transparent)
}
}
SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not())
Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) {
content()
}
},
)
}
// Window utils
@Composable
private fun getDialogWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window
@Composable
private fun getActivityWindow(): Window? = LocalView.current.context.getActivityWindow()
private tailrec fun Context.getActivityWindow(): Window? = when (this) {
is Activity -> window
is ContextWrapper -> baseContext.getActivityWindow()
else -> null
}

View file

@ -75,7 +75,7 @@ fun Modifier.bottomFade(
)
enum class FadePosition {
TOP, BOTTOM, LEFT, RIGHT;
TOP, BOTTOM, LEFT, RIGHT
}
@Stable

View file

@ -169,12 +169,12 @@ private fun Preview_Tree() {
},
content = {
ArrowRowItems(
itemPadding = PaddingValues(vertical = TangemTheme.dimens.spacing4),
items = persistentListOf(
stringReference("Fist item"),
stringReference("Second item"),
stringReference("Third item"),
),
itemPadding = PaddingValues(vertical = TangemTheme.dimens.spacing4),
rootContent = {
PreviewItem(stringReference("Root"))
},

View file

@ -16,11 +16,11 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
inline fun <T : Any> InformationBlockContentScope.ListItems(
items: ImmutableList<T>,
itemContent: @Composable BoxScope.(T) -> Unit,
modifier: Modifier = Modifier,
itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0),
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
verticalArragement: Arrangement.Vertical = Arrangement.Top,
itemContent: @Composable BoxScope.(T) -> Unit,
) {
Column(
modifier = modifier.fillMaxWidth(),
@ -42,10 +42,10 @@ inline fun <T : Any> InformationBlockContentScope.ListItems(
@Composable
inline fun <T : Any> InformationBlockContentScope.GridItems(
items: ImmutableList<T>,
itemContent: @Composable BoxScope.(T) -> Unit,
modifier: Modifier = Modifier,
verticalAlignment: Alignment.Vertical = Alignment.Top,
horizontalArragement: Arrangement.Horizontal = Arrangement.Start,
itemContent: @Composable BoxScope.(T) -> Unit,
) {
val rowItems by remember(items) {
derivedStateOf {
@ -81,10 +81,10 @@ inline fun <T : Any> InformationBlockContentScope.GridItems(
@Composable
inline fun <T : Any> InformationBlockContentScope.ArrowRowItems(
items: ImmutableList<T>,
rootContent: @Composable BoxScope.() -> Unit,
itemContent: @Composable BoxScope.(T) -> Unit,
modifier: Modifier = Modifier,
itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0),
rootContent: @Composable BoxScope.() -> Unit,
itemContent: @Composable BoxScope.(T) -> Unit,
) {
Column(
modifier = modifier.fillMaxWidth(),

View file

@ -48,10 +48,10 @@ const val MODAL_SHEET_MAX_HEIGHT = 0.8f
inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color = TangemTheme.colors.background.primary,
noinline onBack: (() -> Unit)? = null,
skipPartiallyExpanded: Boolean = true,
dismissOnClickOutside: Boolean = true,
scrollableContent: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit = {},
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
@ -202,9 +202,9 @@ inline fun <reified T : TangemBottomSheetConfigContent> BsContent(
inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
config: TangemBottomSheetConfig,
sheetState: SheetState,
modifier: Modifier = Modifier,
noinline onBack: (() -> Unit)? = null,
noinline bsContent: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
) {
if (onBack != null) {
ModalBottomSheetWithBackHandling(

View file

@ -21,7 +21,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.WalletConnectDetailsBottomSheetTestTags
import com.tangem.core.ui.test.BaseBottomSheetTestTags
/**
* Title component for [TangemModalBottomSheet] with [TangemIconButton] for buttons.
@ -58,7 +58,7 @@ fun TangemModalBottomSheetTitle(
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.testTag(WalletConnectDetailsBottomSheetTestTags.TITLE),
.testTag(BaseBottomSheetTestTags.TITLE),
)
}
if (subtitle != null) {
@ -68,7 +68,7 @@ fun TangemModalBottomSheetTitle(
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.testTag(WalletConnectDetailsBottomSheetTestTags.DATE),
.testTag(BaseBottomSheetTestTags.SUBTITLE),
)
}
}
@ -79,7 +79,7 @@ fun TangemModalBottomSheetTitle(
modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterEnd)
.testTag(WalletConnectDetailsBottomSheetTestTags.CLOSE_BUTTON),
.testTag(BaseBottomSheetTestTags.CLOSE_BUTTON),
)
}
}

View file

@ -143,11 +143,11 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
config: TangemBottomSheetConfig,
sheetState: SheetState,
containerColor: Color,
modifier: Modifier = Modifier,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit,
crossinline content: @Composable (T) -> Unit,
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
modifier: Modifier = Modifier,
) {
val model = config.content as? T ?: return

View file

@ -152,10 +152,10 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
sheetState: SheetState,
containerColor: Color,
addBottomInsets: Boolean,
modifier: Modifier = Modifier,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
modifier: Modifier = Modifier,
) {
val model = config.content as? T ?: return

View file

@ -22,7 +22,7 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
import com.tangem.core.ui.test.BaseActionButtonsBlockTestTags
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -36,7 +36,7 @@ fun HorizontalActionChips(
LazyRow(
modifier = modifier
.fillMaxWidth()
.testTag(TokenDetailsScreenTestTags.HORIZONTAL_ACTION_CHIPS),
.testTag(BaseActionButtonsBlockTestTags.HORIZONTAL_ACTION_CHIPS),
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
contentPadding = contentPadding,

View file

@ -34,7 +34,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
import com.tangem.core.ui.test.BaseActionButtonsBlockTestTags
/**
* Rounded action button
@ -100,7 +100,7 @@ fun ActionButton(
),
)
},
modifier = modifier.testTag(TokenDetailsScreenTestTags.ACTION_BUTTON),
modifier = modifier,
color = color,
containerColor = containerColor,
)
@ -111,10 +111,10 @@ fun ActionButton(
fun ActionBaseButton(
config: ActionButtonConfig,
shape: RoundedCornerShape,
content: @Composable (modifier: Modifier) -> Unit,
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.button.secondary,
containerColor: Color = TangemTheme.colors.background.secondary,
content: @Composable (modifier: Modifier) -> Unit,
) {
val context = LocalContext.current
val backgroundColor by animateColorAsState(
@ -145,7 +145,8 @@ fun ActionBaseButton(
}
},
)
.background(color = backgroundColor),
.background(color = backgroundColor)
.testTag(BaseActionButtonsBlockTestTags.ACTION_BUTTON),
) {
content(Modifier.align(Alignment.Center))
@ -163,9 +164,9 @@ fun ActionBaseButton(
@Composable
fun ActionButtonContent(
config: ActionButtonConfig,
text: @Composable (Color) -> Unit,
modifier: Modifier = Modifier,
paddingBetweenIconAndText: Dp = 8.dp,
text: @Composable (Color) -> Unit,
) {
Row(
modifier = modifier

View file

@ -24,8 +24,8 @@ internal inline fun DefaultCurrencyIcon(
size: Dp,
alpha: Float,
colorFilter: ColorFilter?,
crossinline errorIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
crossinline errorIcon: @Composable () -> Unit,
) {
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
var isBackgroundColorDefined by remember { mutableStateOf(false) }

View file

@ -113,8 +113,8 @@ private fun TokenIcon(
url: String?,
alpha: Float,
colorFilter: ColorFilter?,
errorIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
errorIcon: @Composable () -> Unit,
) {
if (url == null) {
errorIcon()

View file

@ -103,11 +103,11 @@ fun SearchBar(
@OptIn(ExperimentalMaterial3Api::class)
private fun DecorationBox(
state: SearchBarUM,
innerTextField: @Composable () -> Unit,
interactionSource: MutableInteractionSource,
colors: TextFieldColors,
focusManager: FocusManager,
keyboardController: SoftwareKeyboardController?,
innerTextField: @Composable () -> Unit,
) {
TextFieldDefaults.DecorationBox(
value = state.query,

View file

@ -121,12 +121,12 @@ fun SimpleTextField(
@Composable
private fun SimpleTextPlaceholder(
placeholder: TextReference?,
value: String,
textStyle: TextStyle,
centered: Boolean,
textValue: @Composable () -> Unit,
placeholder: TextReference?,
color: Color = TangemTheme.colors.text.disabled,
textValue: @Composable () -> Unit,
) {
Box(contentAlignment = if (centered) Alignment.Center else Alignment.TopStart) {
if (value.isBlank() && placeholder != null) {

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.components.icons
import androidx.compose.runtime.Stable
@Stable
enum class IconTint {
Accent,
Warning,
Inactive,
}

View file

@ -58,8 +58,8 @@ internal data class Blockies(
}
private fun dataFromSeed(seed: MutableList<Long>) = MutableList(SIZE * SIZE) { DEFAULT_VALUE_F }.apply {
(0 until SIZE).forEach { row ->
(0 until HALF_SIZE).forEach { column ->
for (row in 0 until SIZE) {
for (column in 0 until HALF_SIZE) {
val value = floor(nextSeed(seed) * PROBABILITY_COLOR)
this[row * SIZE + column] = value
this[(row + 1) * SIZE - column - 1] = value

View file

@ -256,6 +256,7 @@ private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButt
modifier = Modifier.fillMaxWidth(),
iconResId = config.iconResId,
enabled = isEnabled,
showProgress = config.showProgress,
)
} else {
SecondaryButton(
@ -264,6 +265,7 @@ private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButt
modifier = Modifier.fillMaxWidth(),
size = TangemButtonSize.WideAction,
enabled = isEnabled,
showProgress = config.showProgress,
)
}
}

View file

@ -44,6 +44,7 @@ data class NotificationConfig(
val text: TextReference,
@DrawableRes val iconResId: Int? = null,
val onClick: () -> Unit,
val showProgress: Boolean = false,
) : ButtonsState()
data class PairButtonsConfig(

View file

@ -21,9 +21,9 @@ import com.tangem.core.ui.utils.*
@Composable
inline fun ArrowRow(
isLastItem: Boolean,
content: @Composable() (BoxScope.() -> Unit),
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0),
content: @Composable() (BoxScope.() -> Unit),
) {
val density = LocalDensity.current.density
val defaultRowHeight = TangemTheme.dimens.size0

View file

@ -27,7 +27,7 @@ private const val DISABLED_ICON_ALPHA = 0.4f
* [Figma Component](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2737-2800&t=ewlXfWwbDnRhjw4B-4)
* */
@Composable
fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier) {
fun BlockchainRow(model: BlockchainRowUM, modifier: Modifier = Modifier, action: @Composable BoxScope.() -> Unit) {
RowContentContainer(
modifier = modifier
.heightIn(min = TangemTheme.dimens.size52)

View file

@ -53,10 +53,10 @@ fun ChainRow(model: ChainRowUM, modifier: Modifier = Modifier, action: @Composab
@Composable
inline fun ChainRowContainer(
modifier: Modifier = Modifier,
icon: @Composable BoxScope.() -> Unit,
text: @Composable BoxScope.() -> Unit,
action: @Composable BoxScope.() -> Unit,
modifier: Modifier = Modifier,
) {
RowContentContainer(
modifier = modifier

View file

@ -29,8 +29,8 @@ import com.tangem.core.ui.res.TangemThemePreview
*/
@Composable
fun NetworkTitle(
title: @Composable BoxScope.() -> Unit,
modifier: Modifier = Modifier,
title: @Composable BoxScope.() -> Unit,
action: (@Composable BoxScope.() -> Unit)? = null,
) {
val minHeight = if (action == null) TangemTheme.dimens.size36 else TangemTheme.dimens.size40

View file

@ -13,11 +13,11 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
inline fun RowContentContainer(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
icon: @Composable BoxScope.() -> Unit,
text: @Composable BoxScope.() -> Unit,
action: @Composable BoxScope.() -> Unit,
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
Row(
modifier = modifier.fillMaxWidth(),

View file

@ -122,16 +122,18 @@ fun ShowcaseButtons(
private fun Showcase_Preview() {
TangemThemePreview {
Showcase(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerIconRes = R.drawable.ic_notification_56,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
R.drawable.ic_rocket_launch_24,
resourceReference(R.string.user_push_notification_agreement_argument_one),
iconRes = R.drawable.ic_rocket_launch_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_one_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_one_subtitle),
),
ShowcaseItemModel(
R.drawable.ic_storefront_24,
resourceReference(R.string.user_push_notification_agreement_argument_two),
iconRes = R.drawable.ic_storefront_24,
title = resourceReference(R.string.user_push_notification_agreement_argument_two_title),
subTitle = resourceReference(R.string.user_push_notification_agreement_argument_two_subtitle),
),
),
primaryButton = ShowcaseButtonModel(resourceReference(R.string.common_allow), {}),

View file

@ -63,7 +63,8 @@ fun ShowcaseContent(
repeat(showcaseItems.size) { index ->
ShowcaseItem(
iconRes = showcaseItems[index].iconRes,
text = showcaseItems[index].text,
title = showcaseItems[index].title,
subtitle = showcaseItems[index].subTitle,
)
}
}

View file

@ -1,33 +1,47 @@
package com.tangem.core.ui.components.showcase
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.unit.dp
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun ShowcaseItem(@DrawableRes iconRes: Int, text: TextReference) {
Row {
internal fun ShowcaseItem(@DrawableRes iconRes: Int, title: TextReference, subtitle: TextReference) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
Text(
text = text.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = TangemTheme.dimens.spacing20),
)
verticalArrangement = Arrangement.spacedBy(3.dp),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.fillMaxWidth(),
)
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier.fillMaxWidth(),
)
}
}
}

View file

@ -5,5 +5,6 @@ import com.tangem.core.ui.extensions.TextReference
data class ShowcaseItemModel(
@DrawableRes val iconRes: Int,
val text: TextReference,
val title: TextReference,
val subTitle: TextReference,
)

View file

@ -22,6 +22,7 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.audits.AuditLabelUM
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.icons.IconTint
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.internal.*
import com.tangem.core.ui.components.token.state.TokenItemState
@ -488,7 +489,14 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
fiatAmountState = FiatAmountState.Content(
text = "3213123123321312312312312312 $",
icons = persistentListOf(
FiatAmountState.Content.IconUM(R.drawable.ic_error_sync_24, useAccentColor = false),
FiatAmountState.Content.IconUM(
iconRes = R.drawable.img_attention_20,
tint = IconTint.Warning,
),
FiatAmountState.Content.IconUM(
iconRes = R.drawable.ic_error_sync_24,
tint = IconTint.Inactive,
),
stakingIcon,
),
isFlickering = true,
@ -534,7 +542,10 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
TokenItemState.Draggable(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
titleState = TokenItemState.TitleState.Content(
text = stringReference(value = "Polygon"),
earnApy = stringReference("Earn 5%"),
),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"),
),
TokenItemState.Content(
@ -603,7 +614,7 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
companion object {
val stakingIcon = FiatAmountState.Content.IconUM(R.drawable.ic_staking_24, useAccentColor = true)
val stakingIcon = FiatAmountState.Content.IconUM(R.drawable.ic_staking_24, tint = IconTint.Accent)
val coinIconState
get() = CurrencyIconState.CoinIcon(

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.icons.IconTint
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.res.TangemTheme
@ -75,10 +76,10 @@ private fun ContentFiatAmount(
Icon(
modifier = Modifier.size(12.dp),
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
tint = if (icon.useAccentColor) {
TangemTheme.colors.icon.accent
} else {
TangemTheme.colors.icon.inactive
tint = when (icon.tint) {
IconTint.Accent -> TangemTheme.colors.icon.accent
IconTint.Warning -> TangemTheme.colors.icon.attention
IconTint.Inactive -> TangemTheme.colors.icon.inactive
},
contentDescription = null,
)

View file

@ -2,7 +2,9 @@ package com.tangem.core.ui.components.token.internal
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -13,8 +15,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.token.state.TokenItemState.TitleState as TokenTitleState
@ -56,6 +60,11 @@ private fun ContentTitle(state: TokenTitleState.Content, modifier: Modifier = Mo
hasPending = state.hasPending,
modifier = Modifier.align(alignment = Alignment.CenterVertically),
)
YieldSupplyApyLabel(
apy = state.earnApy,
modifier = Modifier.align(alignment = Alignment.CenterVertically),
)
}
}
@ -71,6 +80,25 @@ private fun CurrencyNameText(name: String, isAvailable: Boolean, modifier: Modif
)
}
@Composable
private fun YieldSupplyApyLabel(apy: TextReference?, modifier: Modifier = Modifier) {
AnimatedVisibility(visible = apy != null, modifier = modifier) {
Box(
modifier = modifier.background(
color = TangemTheme.colors.text.accent.copy(alpha = 0.1f),
shape = TangemTheme.shapes.roundedCornersSmall2,
),
) {
Text(
text = apy?.resolveReference().orEmpty(),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.accent,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
)
}
}
}
@Composable
private fun PendingTransactionImage(hasPending: Boolean, modifier: Modifier = Modifier) {
AnimatedVisibility(visible = hasPending, modifier = modifier) {

View file

@ -3,6 +3,7 @@ package com.tangem.core.ui.components.token.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.audits.AuditLabelUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.icons.IconTint
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@ -167,6 +168,7 @@ sealed class TokenItemState {
val text: TextReference,
val hasPending: Boolean = false,
val isAvailable: Boolean = true,
val earnApy: TextReference? = null,
) : TitleState()
data object Loading : TitleState()
@ -204,7 +206,7 @@ sealed class TokenItemState {
data class IconUM(
val iconRes: Int,
val useAccentColor: Boolean,
val tint: IconTint = IconTint.Inactive,
)
}

View file

@ -24,9 +24,9 @@ import kotlinx.coroutines.launch
@Composable
fun TangemTooltip(
text: String,
content: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
content: @Composable (Modifier) -> Unit,
) {
InternalTangemTooltip(
modifier = modifier,
@ -46,9 +46,9 @@ fun TangemTooltip(
@Composable
fun TangemTooltip(
text: AnnotatedString,
content: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
content: @Composable (Modifier) -> Unit,
) {
InternalTangemTooltip(
modifier = modifier,
@ -68,10 +68,10 @@ fun TangemTooltip(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun InternalTangemTooltip(
tooltipContent: @Composable () -> Unit,
content: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
tooltipContent: @Composable () -> Unit,
content: @Composable (Modifier) -> Unit,
) {
val tooltipState = rememberTooltipState(isPersistent = true)
val coroutineScope = rememberCoroutineScope()

View file

@ -9,6 +9,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.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -18,6 +19,7 @@ import com.tangem.core.ui.components.buttons.actions.ActionButton
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.EmptyTransactionBlockTestTags
/**
* Placeholder for transaction's block without content
@ -31,19 +33,24 @@ fun EmptyTransactionBlock(state: EmptyTransactionsBlockState, modifier: Modifier
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(color = TangemTheme.colors.background.primary)
.padding(vertical = TangemTheme.dimens.spacing24),
.padding(vertical = TangemTheme.dimens.spacing24)
.testTag(EmptyTransactionBlockTestTags.BLOCK),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size64),
modifier = Modifier
.size(TangemTheme.dimens.size64)
.testTag(EmptyTransactionBlockTestTags.ICON),
painter = painterResource(id = state.iconRes),
tint = TangemTheme.colors.icon.inactive,
contentDescription = null,
)
Text(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing32),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing32)
.testTag(EmptyTransactionBlockTestTags.TEXT),
textAlign = TextAlign.Center,
text = state.text.resolveReference(),
style = TangemTheme.typography.body2,
@ -51,7 +58,9 @@ fun EmptyTransactionBlock(state: EmptyTransactionsBlockState, modifier: Modifier
)
Buttons(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing18),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing18)
.testTag(EmptyTransactionBlockTestTags.EXPLORE_BUTTON),
state = state.buttonsState,
)
}

View file

@ -10,13 +10,16 @@ interface ComposableBottomSheetComponent {
@Composable
fun BottomSheet()
}
fun getEmptyComposableBottomSheetComponent() = EmptyComposableBottomSheetComponent
companion object {
val EMPTY = EmptyComposableBottomSheetComponent
}
}
object EmptyComposableBottomSheetComponent : ComposableBottomSheetComponent {
override fun dismiss() {}
@Composable
override fun BottomSheet() {}
override fun BottomSheet() {
}
}

View file

@ -9,9 +9,11 @@ fun interface ComposableContentComponent {
@Composable
fun Content(modifier: Modifier)
}
fun getEmptyComposableContentComponent() = EmptyComposableContentComponent
companion object {
val EMPTY = EmptyComposableContentComponent
}
}
object EmptyComposableContentComponent : ComposableContentComponent {
@Composable

View file

@ -15,9 +15,11 @@ interface ComposableModularContentComponent {
@Composable
fun Footer()
}
fun getEmptyComposableModularContentComponent() = EmptyComposableModularContentComponent
companion object {
val EMPTY = EmptyComposableModularContentComponent
}
}
object EmptyComposableModularContentComponent : ComposableModularContentComponent {
@Composable

View file

@ -52,10 +52,10 @@ fun TangemTheme(
@Composable
fun TangemTheme(
isDark: Boolean = false,
windowSize: WindowSize,
typography: TangemTypography = TangemTheme.typography,
dimens: TangemDimens = TangemTheme.dimens,
isDark: Boolean = false,
vibratorHapticManager: VibratorHapticManager? = null,
eventMessageHandler: EventMessageHandler = remember { EventMessageHandler() },
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },

View file

@ -1,11 +1,53 @@
package com.tangem.core.ui.security
import android.app.Activity
import android.view.WindowManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalContext
import com.tangem.core.ui.utils.findActivity
import timber.log.Timber
private val LocalSecureFlagController = staticCompositionLocalOf<SecureFlagController> {
error("No SecureFlagController provided")
}
private class SecureFlagController(private val activity: Activity) {
private var count by mutableIntStateOf(0)
fun enable() {
if (count == 0) {
activity.window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
}
count++
}
fun disable() {
count--
if (count == 0) {
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}
@Composable
fun ProvideSecureFlagController(content: @Composable () -> Unit) {
val activity = LocalContext.current.findActivity()
val controller = remember(activity) { SecureFlagController(activity) }
CompositionLocalProvider(
LocalSecureFlagController provides controller,
content = content,
)
}
/**
* Disables screenshots for the current composition.
@ -14,18 +56,10 @@ import timber.log.Timber
*/
@Composable
fun DisableScreenshotsDisposableEffect() {
val activity = LocalContext.current.findActivity()
val secureFlagController = LocalSecureFlagController.current
DisposableEffect(activity) {
Timber.d("Security mode: enabled")
activity.window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
onDispose {
Timber.d("Security mode: disabled")
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
DisposableEffect(secureFlagController) {
secureFlagController.enable()
onDispose { secureFlagController.disable() }
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object BaseActionButtonsBlockTestTags {
const val HORIZONTAL_ACTION_CHIPS = "BASE_ACTION_BUTTONS_BLOCK_HORIZONTAL_ACTION_CHIPS"
const val ACTION_BUTTON = "BASE_ACTION_BUTTONS_BLOCK_ACTION_BUTTON"
}

View file

@ -2,4 +2,7 @@ package com.tangem.core.ui.test
object BaseBottomSheetTestTags {
const val ACTION_TITLE = "BASE_BOTTOM_SHEET_ACTION_TITLE"
const val TITLE = "BASE_BOTTOM_SHEET_TITLE"
const val SUBTITLE = "BASE_BOTTOM_SHEET_SUBTITLE"
const val CLOSE_BUTTON = "BASE_BOTTOM_SHEET_CLOSE_BUTTON"
}

View file

@ -5,12 +5,4 @@ object BuyTokenDetailsScreenTestTags {
const val FIAT_CURRENCY_ICON = "BUY_TOKEN_DETAILS_SCREEN_FIAT_CURRENCY_ICON"
const val FIAT_AMOUNT_TEXT_FIELD = "BUY_TOKEN_DETAILS_SCREEN_FIAT_AMOUNT_TEXT_FIELD"
const val TOKEN_AMOUNT = "BUY_TOKEN_DETAILS_SCREEN_TOKEN_AMOUNT"
const val PROVIDER_LOADING_TITLE = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_LOADING_TITLE"
const val PROVIDER_LOADING_TEXT = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_LOADING_TITLE"
const val PROVIDER_TITLE = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_TITLE"
const val PROVIDER_TEXT = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_TEXT"
const val TOS_BLOCK = "BUY_TOKEN_DETAILS_SCREEN_TOS_BLOCK"
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.ui.test
object EmptyTransactionBlockTestTags {
const val BLOCK = "EMPTY_TRANSACTION_BLOCK"
const val ICON = "EMPTY_TRANSACTION_BLOCK_ICON"
const val TEXT = "EMPTY_TRANSACTION_BLOCK_TEXT"
const val EXPLORE_BUTTON = "EMPTY_TRANSACTION_BLOCK_EXPLORE_BUTTON"
}

View file

@ -7,9 +7,9 @@ object MainScreenTestTags {
const val TOKEN_LIST_ITEM = "MAIN_SCREEN_TOKEN_LIST_ITEM"
const val WALLET_LIST_ITEM = "MAIN_SCREEN_WALLET_LIST_ITEM"
const val ORGANIZE_TOKENS_BUTTON = "MAIN_SCREEN_ORGANIZE_TOKENS_BUTTON"
const val MULTI_CURRENCY_ACTION_BUTTON = "MAIN_SCREEN_MULTI_CURRENCY_ACTION_BUTTON"
const val CARD_TITLE = "MAIN_SCREEN_CARD_TITLE"
const val CARD_IMAGE = "MAIN_SCREEN_CARD_IMAGE"
const val DEVICES_COUNT = "MAIN_SCREEN_DEVICES_COUNT"
const val WALLET_BALANCE = "MAIN_SCREEN_WALLET_BALANCE"
const val TOTAL_BALANCE_MENU_ITEM = "MAIN_SCREEN_TOTAL_BALANCE_MENU_ITEM"

View file

@ -0,0 +1,12 @@
package com.tangem.core.ui.test
object OnrampOffersBlockTestTags {
const val PROVIDER_NAME = "ONRAMP_OFFERS_BLOCK_PROVIDER_TITLE"
const val BEST_RATE_ICON = "ONRAMP_OFFERS_BLOCK_BEST_RATE_ICON"
const val BEST_RATE_TITLE = "ONRAMP_OFFERS_BLOCK_BEST_RATE_TITLE"
const val OFFER_TOKEN_AMOUNT = "ONRAMP_OFFERS_BLOCK_OFFER_TOKEN_AMOUNT"
const val TIMING_ICON = "ONRAMP_OFFERS_BLOCK_TIMING_ICON"
const val PAY_WITH = "ONRAMP_OFFERS_BLOCK_PAY_WITH"
const val PAYMENT_METHOD_ICON = "ONRAMP_OFFERS_BLOCK_PAYMENT_METHOD_ICON"
const val TIMING_TEXT = "ONRAMP_OFFERS_BLOCK_TIMING_TEXT"
}

View file

@ -1,7 +1,13 @@
package com.tangem.core.ui.test
object SelectPaymentMethodBottomSheetTestTags {
const val LAZY_LIST = "SELECT_PAYMENT_METHOD_LAZY_LIST"
const val PAYMENT_METHOD_ICON = "PAYMENT_METHOD_NAME"
const val PAYMENT_METHOD = "SELECT_PAYMENT_METHOD_BOTTOM_SHEET_PAYMENT_METHOD"
const val PAYMENT_METHOD_ICON = "SELECT_PAYMENT_METHOD_BOTTOM_SHEET_PAYMENT_METHOD_ICON"
const val PAYMENT_METHOD_NAME = "SELECT_PAYMENT_METHOD_BOTTOM_SHEET_PAYMENT_METHOD_NAME"
const val UP_TO_TEXT = "SELECT_PAYMENT_METHOD_BOTTOM_SHEET_UP_TO_TEXT"
const val TOKEN_AMOUNT = "SELECT_PAYMENT_METHOD_BOTTOM_SHEET_TOKEN_AMOUNT"
const val BEST_RATE_ICON = "SELECT_PAYMENT_METHOD_BOTTOM_SHEET_BEST_RATE_ICON"
const val PROVIDER_COUNT_ICON = "SELECT_PAYMENT_METHOD_BOTTOM_SHEET_PROVIDER_COUNT_ICON"
const val PROVIDER_COUNT_TEXT = "SELECT_PAYMENT_METHOD_BOTTOM_SHEET_PROVIDER_COUNT_TEXT"
const val TIMING_ICON = "SELECT_PAYMENT_METHOD_BOTTOM_SHEET_TIMING_ICON"
}

View file

@ -6,4 +6,6 @@ object StoriesScreenTestTags {
const val ORDER_BUTTON = "STORIES_SCREEN_ORDER_BUTTON"
const val CREATE_NEW_WALLET_BUTTON = "STORIES_SCREEN_CREATE_NEW_WALLET_BUTTON"
const val ADD_EXISTING_WALLET_BUTTON = "STORIES_SCREEN_ADD_EXISTING_WALLET_BUTTON"
const val TITLE = "STORIES_SCREEN_TITLE"
const val TEXT = "STORIES_SCREEN_TEXT"
}

View file

@ -4,8 +4,6 @@ object TokenDetailsScreenTestTags {
const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER"
const val TOKEN_TITLE = "TOKEN_DETAILS_SCREEN_TOKEN_TITLE"
const val ACTION_BUTTON = "TOKEN_DETAILS_SCREEN_ACTION_BUTTON"
const val HORIZONTAL_ACTION_CHIPS = "TOKEN_DETAILS_SCREEN_HORIZONTAL_ACTION_CHIPS"
const val STAKING_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_BLOCK"
const val STAKING_AVAILABLE_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_AVAILABLE_BLOCK"

View file

@ -1,9 +1,6 @@
package com.tangem.core.ui.test
object WalletConnectDetailsBottomSheetTestTags {
const val TITLE = "WALLET_CONNECT_DETAILS_BOTTOM_SHEET_TITLE"
const val DATE = "WALLET_CONNECT_DETAILS_BOTTOM_SHEET_DATE"
const val CLOSE_BUTTON = "WALLET_CONNECT_DETAILS_BOTTOM_CLOSE_BUTTON"
const val DISCONNECT_BUTTON = "WALLET_CONNECT_DETAILS_BOTTOM_SHEET_DISCONNECT_BUTTON"
const val WALLET_ICON = "WALLET_CONNECT_DETAILS_BOTTOM_WALLET_ICON"
const val WALLET_TITLE = "WALLET_CONNECT_DETAILS_BOTTOM_WALLET"

View file

@ -64,9 +64,7 @@ fun DecimalFormat.getValidatedNumberWithFixedDecimals(text: String, decimals: In
val beforeDecimal = filteredChars.substringBefore(decimalSeparator)
val afterDecimal = filteredChars.substringAfter(decimalSeparator)
decimals.getWithIntegerDecimals(beforeDecimal, decimalSeparator, afterDecimal)
}
// If there is no dot, just take all digits
else {
} else { // If there is no dot, just take all digits
filteredChars
}
}
@ -87,9 +85,7 @@ fun DecimalFormat.formatWithThousands(text: String, decimals: Int): String {
.reversed()
val afterDecimal = localizedText.substringAfter(decimalSeparator)
decimals.getWithIntegerDecimals(beforeDecimal, decimalSeparator, afterDecimal)
}
// If there is no dot, just take all digits
else {
} else { // If there is no dot, just take all digits
localizedText.reversed()
.chunked(TEXT_CHUNK_THOUSAND)
.joinToString(thousandsSeparator.toString())

View file

@ -44,9 +44,7 @@ class InputNumberFormatter(
val beforeDecimal = filteredChars.substringBefore(decimalSeparator)
val afterDecimal = filteredChars.substringAfter(decimalSeparator)
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
}
// If there is no dot, just take all digits
else {
} else { // If there is no dot, just take all digits
filteredChars
}
}
@ -62,9 +60,7 @@ class InputNumberFormatter(
.reversed()
val afterDecimal = text.substringAfter(decimalSeparator)
beforeDecimal + decimalSeparator + afterDecimal.take(decimals)
}
// If there is no dot, just take all digits
else {
} else { // If there is no dot, just take all digits
text.reversed()
.chunked(TEXT_CHUNK_THOUSAND)
.joinToString(thousandsSeparator.toString())

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M5.353,1.622C5.641,1.123 6.362,1.123 6.65,1.622L10.85,8.875C11.139,9.375 10.778,10.001 10.2,10.001H1.802C1.224,10.001 0.864,9.375 1.153,8.875L5.353,1.622ZM5.502,8.002V9.002H6.502V8.002H5.502ZM5.502,4.002V7.002H6.502V4.002H5.502Z"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<path
android:pathData="M10.289,26.72C10.784,17.3 18.567,9.917 28,9.917C37.433,9.917 45.216,17.3 45.711,26.72C45.733,27.123 45.748,27.508 45.764,27.879L45.764,27.88C45.815,29.143 45.86,30.243 46.089,31.298C46.363,32.557 46.869,33.548 47.898,34.32C49.694,35.666 50.75,37.779 50.75,40.023C50.75,43.246 48.213,46.083 44.8,46.083H11.2C7.787,46.083 5.25,43.246 5.25,40.023C5.25,37.779 6.306,35.666 8.102,34.32C9.131,33.548 9.637,32.557 9.911,31.298C10.14,30.243 10.185,29.143 10.236,27.88L10.236,27.88C10.252,27.508 10.267,27.123 10.289,26.72Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M24.567,3.207C25.605,2.527 26.873,2.333 28,2.333C29.127,2.333 30.395,2.527 31.433,3.207C32.586,3.961 33.25,5.182 33.25,6.708C33.25,8.099 32.706,9.552 31.879,10.665C31.065,11.76 29.73,12.833 28,12.833C26.27,12.833 24.935,11.76 24.121,10.665C23.294,9.552 22.75,8.099 22.75,6.708C22.75,5.182 23.414,3.961 24.567,3.207Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M21,42C22.288,42 23.333,43.045 23.333,44.333C23.333,46.911 25.423,49 28,49C30.577,49 32.667,46.911 32.667,44.333C32.667,43.045 33.711,42 35,42C36.289,42 37.333,43.045 37.333,44.333C37.333,49.488 33.154,53.667 28,53.667C22.845,53.667 18.667,49.488 18.667,44.333C18.667,43.045 19.711,42 21,42Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="25dp"
android:viewportWidth="24"
android:viewportHeight="25">
<path
android:pathData="M14.75,5.75C14.75,3.541 16.541,1.75 18.75,1.75C20.959,1.75 22.75,3.541 22.75,5.75C22.75,7.959 20.959,9.75 18.75,9.75C16.541,9.75 14.75,7.959 14.75,5.75Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M12.663,2.75C13.279,2.75 13.587,2.75 13.707,2.914C13.827,3.078 13.72,3.412 13.508,4.08C13.34,4.607 13.25,5.168 13.25,5.75C13.25,8.788 15.712,11.25 18.75,11.25C19.339,11.25 19.907,11.157 20.44,10.985C21.091,10.775 21.418,10.671 21.582,10.79C21.746,10.91 21.747,11.209 21.749,11.809C21.75,12.172 21.75,12.55 21.75,12.942V13.058C21.75,15.248 21.75,16.968 21.569,18.312C21.384,19.688 20.997,20.781 20.139,21.639C19.281,22.497 18.188,22.884 16.812,23.069C15.468,23.25 13.748,23.25 11.558,23.25H11.442C9.252,23.25 7.532,23.25 6.188,23.069C4.812,22.884 3.72,22.497 2.861,21.639C2.003,20.781 1.616,19.688 1.431,18.312C1.25,16.968 1.25,15.248 1.25,13.058V12.942C1.25,10.752 1.25,9.032 1.431,7.688C1.616,6.312 2.003,5.22 2.861,4.361C3.72,3.503 4.812,3.116 6.188,2.931C7.532,2.75 9.252,2.75 11.442,2.75H12.663ZM7,15.75C6.586,15.75 6.25,16.086 6.25,16.5C6.25,16.914 6.586,17.25 7,17.25H15C15.414,17.25 15.75,16.914 15.75,16.5C15.75,16.086 15.414,15.75 15,15.75H7ZM7,10.75C6.586,10.75 6.25,11.086 6.25,11.5C6.25,11.914 6.586,12.25 7,12.25H11C11.414,12.25 11.75,11.914 11.75,11.5C11.75,11.086 11.414,10.75 11,10.75H7Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
android:strokeAlpha="0.1"
android:fillColor="#0099FF"
android:fillAlpha="0.1"/>
<path
android:pathData="M17.758,21.123C18.172,21.12 18.51,21.453 18.514,21.867C18.517,22.281 18.184,22.619 17.771,22.623C16.646,22.633 15.542,23.074 14.779,23.887C14.496,24.189 14.021,24.203 13.719,23.92C13.417,23.636 13.402,23.161 13.686,22.859C14.756,21.719 16.265,21.136 17.758,21.123ZM18.982,17.373C18.982,16.671 18.425,16.123 17.764,16.123C17.102,16.123 16.546,16.672 16.546,17.373C16.546,18.075 17.102,18.623 17.764,18.623C18.425,18.623 18.982,18.075 18.982,17.373ZM20.482,17.373C20.482,18.881 19.276,20.123 17.764,20.123C16.251,20.123 15.046,18.881 15.046,17.373C15.046,15.866 16.251,14.623 17.764,14.623C19.276,14.623 20.482,15.866 20.482,17.373Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M8.482,20.742V16.006C8.482,14.465 8.481,13.23 8.618,12.262C8.759,11.268 9.06,10.447 9.742,9.801C10.346,9.229 11.088,8.946 11.974,8.799C12.841,8.655 13.915,8.63 15.229,8.625C15.644,8.624 15.981,8.958 15.982,9.372C15.984,9.786 15.649,10.123 15.235,10.125C13.905,10.13 12.95,10.157 12.22,10.278C11.508,10.397 11.086,10.594 10.773,10.89C10.431,11.215 10.219,11.661 10.104,12.473C9.985,13.311 9.982,14.421 9.982,16.006V20.742C9.982,22.328 9.985,23.437 10.104,24.275C10.219,25.087 10.431,25.534 10.773,25.858C11.122,26.189 11.61,26.397 12.486,26.509C13.381,26.623 14.562,26.624 16.232,26.624H18.232C18.647,26.624 18.982,26.96 18.982,27.374C18.982,27.788 18.647,28.124 18.232,28.124H16.232C14.603,28.124 13.308,28.125 12.297,27.996C11.268,27.865 10.418,27.588 9.742,26.947C9.06,26.301 8.759,25.48 8.618,24.486C8.481,23.519 8.482,22.283 8.482,20.742ZM25.482,16.874V16.006C25.482,14.421 25.48,13.311 25.361,12.473C25.246,11.661 25.034,11.215 24.691,10.89C24.379,10.594 23.956,10.397 23.245,10.278C22.515,10.157 21.56,10.13 20.229,10.125C19.815,10.123 19.481,9.786 19.482,9.372C19.484,8.958 19.821,8.624 20.235,8.625C21.549,8.63 22.624,8.655 23.491,8.799C24.377,8.946 25.119,9.229 25.723,9.801C26.405,10.447 26.706,11.268 26.847,12.262C26.984,13.23 26.982,14.465 26.982,16.006V16.874C26.982,17.288 26.647,17.624 26.232,17.624C25.818,17.624 25.483,17.288 25.482,16.874Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M17.733,6.623C18.164,6.623 18.503,6.618 18.81,6.689C19.473,6.841 20.057,7.255 20.383,7.855C20.536,8.136 20.606,8.466 20.691,8.836L20.774,9.195C20.856,9.547 20.931,9.869 20.964,10.138C20.998,10.417 21,10.74 20.854,11.062C20.704,11.391 20.455,11.659 20.153,11.843C19.867,12.016 19.558,12.073 19.27,12.099C18.986,12.124 18.634,12.123 18.231,12.123H17.235C16.831,12.123 16.48,12.124 16.195,12.099C15.907,12.073 15.598,12.016 15.313,11.843C15.01,11.659 14.761,11.391 14.611,11.062C14.465,10.74 14.467,10.417 14.501,10.138C14.534,9.869 14.609,9.547 14.691,9.195L14.774,8.836C14.859,8.466 14.929,8.136 15.082,7.855C15.408,7.255 15.992,6.841 16.655,6.689C16.962,6.618 17.301,6.623 17.733,6.623ZM17.733,8.123C17.223,8.123 17.092,8.128 16.992,8.15C16.71,8.215 16.503,8.382 16.399,8.571C16.368,8.629 16.342,8.714 16.236,9.174L16.153,9.534C16.064,9.918 16.011,10.148 15.99,10.318C15.981,10.391 15.982,10.432 15.983,10.45C16.002,10.486 16.035,10.527 16.091,10.562C16.092,10.562 16.095,10.563 16.098,10.564C16.104,10.567 16.115,10.571 16.133,10.575C16.17,10.585 16.23,10.596 16.326,10.605C16.53,10.622 16.804,10.623 17.235,10.623H18.231C18.661,10.623 18.935,10.622 19.139,10.605C19.235,10.596 19.295,10.585 19.332,10.575C19.35,10.571 19.361,10.567 19.367,10.564C19.37,10.563 19.372,10.562 19.373,10.562C19.429,10.528 19.462,10.486 19.481,10.45C19.482,10.432 19.484,10.392 19.475,10.318C19.454,10.148 19.401,9.918 19.313,9.534L19.23,9.174C19.123,8.714 19.097,8.629 19.066,8.571C18.962,8.382 18.755,8.215 18.473,8.15C18.373,8.128 18.242,8.123 17.733,8.123Z"
android:fillColor="#0099FF"/>
<path
android:pathData="M27.483,23.373C27.483,21.578 26.028,20.123 24.233,20.123C22.438,20.123 20.983,21.578 20.983,23.373C20.983,25.168 22.438,26.623 24.233,26.623C26.028,26.623 27.483,25.168 27.483,23.373ZM22.572,24.493H22.574L22.575,24.494C22.202,24.316 22.045,23.868 22.223,23.494C22.401,23.121 22.848,22.963 23.222,23.141L23.224,23.142L23.227,23.143C23.228,23.143 23.23,23.145 23.232,23.146C23.235,23.147 23.24,23.149 23.244,23.151C23.253,23.156 23.265,23.162 23.278,23.169C23.303,23.183 23.337,23.202 23.376,23.226C23.451,23.272 23.549,23.338 23.658,23.426C23.669,23.409 23.68,23.393 23.691,23.376C24.021,22.875 24.552,22.178 25.21,21.824C25.575,21.628 26.03,21.764 26.227,22.129C26.423,22.494 26.286,22.948 25.921,23.145C25.618,23.308 25.26,23.722 24.942,24.203C24.795,24.427 24.673,24.637 24.589,24.791C24.547,24.868 24.515,24.93 24.493,24.972C24.483,24.993 24.475,25.009 24.47,25.019C24.467,25.024 24.466,25.027 24.465,25.029C24.345,25.279 24.097,25.444 23.82,25.456C23.544,25.468 23.282,25.326 23.142,25.087C23.005,24.855 22.847,24.698 22.729,24.603C22.67,24.555 22.622,24.523 22.593,24.505L22.572,24.493ZM28.983,23.373C28.983,25.997 26.856,28.123 24.233,28.123C21.609,28.123 19.483,25.997 19.483,23.373C19.483,20.75 21.609,18.623 24.233,18.623C26.856,18.623 28.983,20.75 28.983,23.373Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="25dp"
android:viewportWidth="24"
android:viewportHeight="25">
<path
android:pathData="M12,2.75C12.414,2.75 12.75,3.086 12.75,3.5C12.75,5.506 13.761,7.575 15.343,9.157C16.925,10.739 18.994,11.75 21,11.75C21.414,11.75 21.75,12.086 21.75,12.5C21.75,12.914 21.414,13.25 21,13.25C18.994,13.25 16.925,14.261 15.343,15.843C13.761,17.425 12.75,19.494 12.75,21.5C12.75,21.914 12.414,22.25 12,22.25C11.586,22.25 11.25,21.914 11.25,21.5C11.25,19.494 10.239,17.425 8.657,15.843C7.075,14.261 5.006,13.25 3,13.25C2.586,13.25 2.25,12.914 2.25,12.5C2.25,12.086 2.586,11.75 3,11.75C5.006,11.75 7.075,10.739 8.657,9.157C10.239,7.575 11.25,5.506 11.25,3.5C11.25,3.086 11.586,2.75 12,2.75Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M19.25,1.75C19.463,1.75 19.648,1.898 19.695,2.106L19.93,3.146C20.09,3.856 20.644,4.41 21.354,4.57L22.394,4.805C22.602,4.852 22.75,5.037 22.75,5.25C22.75,5.463 22.602,5.648 22.394,5.695L21.354,5.93C20.644,6.09 20.09,6.644 19.93,7.354L19.695,8.394C19.648,8.602 19.463,8.75 19.25,8.75C19.037,8.75 18.852,8.602 18.805,8.394L18.57,7.354C18.41,6.644 17.856,6.09 17.146,5.93L16.106,5.695C15.898,5.648 15.75,5.463 15.75,5.25C15.75,5.037 15.898,4.852 16.106,4.805L17.146,4.57C17.856,4.41 18.41,3.856 18.57,3.146L18.805,2.106C18.852,1.898 19.037,1.75 19.25,1.75Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M4.75,16.25C4.963,16.25 5.148,16.398 5.195,16.606L5.43,17.646C5.59,18.356 6.144,18.91 6.854,19.07L7.894,19.305C8.102,19.352 8.25,19.537 8.25,19.75C8.25,19.963 8.102,20.148 7.894,20.195L6.854,20.43C6.144,20.59 5.59,21.144 5.43,21.854L5.195,22.894C5.148,23.102 4.963,23.25 4.75,23.25C4.537,23.25 4.352,23.102 4.305,22.894L4.07,21.854C3.91,21.144 3.356,20.59 2.646,20.43L1.606,20.195C1.398,20.148 1.25,19.963 1.25,19.75C1.25,19.537 1.398,19.352 1.606,19.305L2.646,19.07C3.356,18.91 3.91,18.356 4.07,17.646L4.305,16.606C4.352,16.398 4.537,16.25 4.75,16.25Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0"
android:strokeAlpha="0.1"
android:fillColor="#0099FF"
android:fillAlpha="0.1"/>
<path
android:pathData="M9,10L27,10A3,3 0,0 1,30 13L30,23A3,3 0,0 1,27 26L9,26A3,3 0,0 1,6 23L6,13A3,3 0,0 1,9 10z"
android:fillColor="#0099FF"/>
<path
android:pathData="M13.051,20.582H11.502L10.34,16.5C10.285,16.312 10.167,16.146 9.995,16.068C9.565,15.871 9.092,15.715 8.575,15.636V15.479H11.071C11.415,15.479 11.674,15.715 11.717,15.989L12.32,18.933L13.868,15.479H15.375L13.051,20.582ZM16.235,20.58H14.771L15.976,15.477H17.44L16.235,20.58ZM19.335,16.891C19.378,16.616 19.636,16.459 19.938,16.459C20.412,16.419 20.927,16.498 21.358,16.694L21.617,15.595C21.186,15.438 20.712,15.359 20.282,15.359C18.862,15.359 17.829,16.066 17.829,17.047C17.829,17.794 18.561,18.185 19.077,18.422C19.636,18.657 19.852,18.814 19.809,19.049C19.809,19.403 19.378,19.56 18.948,19.56C18.432,19.56 17.915,19.442 17.442,19.246L17.184,20.345C17.7,20.541 18.259,20.62 18.776,20.62C20.368,20.659 21.358,19.952 21.358,18.892C21.358,17.558 19.335,17.479 19.335,16.891ZM26.48,20.58L25.318,15.477H24.07C23.811,15.477 23.553,15.634 23.467,15.869L21.315,20.58H22.822L23.122,19.834H24.973L25.146,20.58H26.48ZM24.283,16.852L24.713,18.775H23.508L24.283,16.852Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

Some files were not shown because too many files have changed in this diff Show more