Updated on 2026-08-14
This commit is contained in:
commit
7e48e67354
942 changed files with 40912 additions and 7544 deletions
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.core.analytics.models
|
||||
|
||||
const val IS_NOT_HTTP_ERROR = "Is not http error"
|
||||
|
||||
sealed class AnalyticsParam {
|
||||
|
||||
sealed class CardBalanceState(val value: String) {
|
||||
|
|
@ -77,7 +79,9 @@ sealed class AnalyticsParam {
|
|||
data object Backup : ScreensSources("Backup")
|
||||
data object Onboarding : ScreensSources("Onboarding")
|
||||
data object LongTap : ScreensSources("Long Tap")
|
||||
data object Market : ScreensSources("Market")
|
||||
data object Markets : ScreensSources("Markets")
|
||||
data object MarketPulse : ScreensSources("Market Pulse")
|
||||
data object TangemPay : ScreensSources("Tangem Pay")
|
||||
data object WalletSettings : ScreensSources("Wallet Settings")
|
||||
data object Upgrade : ScreensSources("Upgrade")
|
||||
|
|
@ -86,6 +90,9 @@ sealed class AnalyticsParam {
|
|||
data object CreateWalletIntro : ScreensSources("Create Wallet Intro")
|
||||
data object AddNewWallet : ScreensSources("Add New Wallet")
|
||||
data object CreateWallet : ScreensSources("Create Wallet")
|
||||
data object NewsList : ScreensSources("News List")
|
||||
data object NewsLink : ScreensSources("News Link")
|
||||
data object NewsPage : ScreensSources("News Page")
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.core.analytics.models
|
||||
|
||||
/**
|
||||
* Marker interface for analytics events that should be sent only once per session.
|
||||
*
|
||||
* Events implementing this interface will be tracked by their [oneTimeEventId] to ensure
|
||||
* they are not sent multiple times during the same application session. Once an event
|
||||
* with a specific [oneTimeEventId] has been sent, subsequent attempts to send an event
|
||||
* with the same ID will be ignored.
|
||||
*
|
||||
* @see Analytics.send
|
||||
*/
|
||||
interface OneTimePerSessionEvent {
|
||||
/**
|
||||
* Unique identifier for the one-time event.
|
||||
*
|
||||
* This ID is used to track whether the event has already been sent in the current session.
|
||||
* Events with the same [oneTimeEventId] will only be sent once, even if they are
|
||||
* different instances of the same event class.
|
||||
*/
|
||||
val oneTimeEventId: String
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.common.extensions.toHexString
|
|||
import com.tangem.core.analytics.api.*
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.analytics.models.OneTimePerSessionEvent
|
||||
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
|
@ -31,6 +32,7 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
|
||||
private val handlers = mutableMapOf<String, AnalyticsHandler>()
|
||||
private val paramsInterceptors = ConcurrentHashMap<String, ParamsInterceptor>()
|
||||
private val oneEventsPerSession = ConcurrentHashMap<String, Boolean>()
|
||||
private val analyticsFilters = mutableSetOf<AnalyticsEventFilter>()
|
||||
private val analyticsMutex = Mutex()
|
||||
|
||||
|
|
@ -85,6 +87,11 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
analyticsScope.launch {
|
||||
if (event is OneTimePerSessionEvent &&
|
||||
oneEventsPerSession.putIfAbsent(event.oneTimeEventId, true) != null
|
||||
) {
|
||||
return@launch
|
||||
}
|
||||
event.params = applyParamsInterceptors(event)
|
||||
val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) }
|
||||
|
||||
|
|
|
|||
|
|
@ -59,16 +59,32 @@
|
|||
"name": "YIELD_SUPPLY_FEATURE_ENABLED",
|
||||
"version": "5.30.0"
|
||||
},
|
||||
{
|
||||
"name": "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED",
|
||||
"version": "5.33.0"
|
||||
},
|
||||
{
|
||||
"name": "NEW_ONRAMP_MAIN_ENABLED",
|
||||
"version": "5.31.0"
|
||||
},
|
||||
{
|
||||
"name": "ACCOUNTS_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
"version": "5.33.0"
|
||||
},
|
||||
{
|
||||
"name": "FEED_ENABLED",
|
||||
"version": "5.33.0"
|
||||
},
|
||||
{
|
||||
"name": "APP_REDESIGN_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "GASLESS_TRANSACTIONS_ENABLED",
|
||||
"version": "5.33.0"
|
||||
},
|
||||
{
|
||||
"name": "SWAP_MARKET_LIST_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,5 @@
|
|||
<ID>NestedScopeFunctions:RetrofitApiBuilder.kt$RetrofitApiBuilder$let { withWriteTimeout(timeout = it.duration, unit = it.unit) }</ID>
|
||||
<ID>UnreachableCode:MockApiConfigsManager.kt$MockApiConfigsManager$apiConfigs + (apiConfig to environment)</ID>
|
||||
<ID>UnreachableCode:MockApiConfigsManager.kt$MockApiConfigsManager$val apiConfig = apiConfigs.keys.firstOrNull { it.id.name == id } ?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI")</ID>
|
||||
<ID>UnusedImports:NetworkModule.kt$import com.tangem.datasource.api.common.config.MoonPay</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ interface AuthProvider {
|
|||
|
||||
fun getApiKey(apiEnvironment: Provider<ApiEnvironment>): ProviderSuspend<String>
|
||||
|
||||
fun getGaslessServiceApiKey(apiEnvironment: Provider<ApiEnvironment>): ProviderSuspend<String>
|
||||
|
||||
/**
|
||||
* Returns map where keys(cardId) associated with cardPublicKey
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ sealed class ApiConfig {
|
|||
YieldSupply,
|
||||
MoonPay,
|
||||
News,
|
||||
GaslessTxService,
|
||||
}
|
||||
|
||||
private fun initializeId(): ID {
|
||||
|
|
@ -39,12 +40,13 @@ sealed class ApiConfig {
|
|||
is TangemTech -> ID.TangemTech
|
||||
is StakeKit -> ID.StakeKit
|
||||
is P2PEthPool -> ID.P2PEthPool
|
||||
is TangemPay -> ID.TangemPay
|
||||
is TangemPayAuth -> ID.TangemPayAuth
|
||||
is TangemPay.Bff -> ID.TangemPay
|
||||
is TangemPay.Auth -> ID.TangemPayAuth
|
||||
is BlockAid -> ID.BlockAid
|
||||
is YieldSupply -> ID.YieldSupply
|
||||
is MoonPay -> ID.MoonPay
|
||||
is News -> ID.News
|
||||
is GaslessTxService -> ID.GaslessTxService
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
/**
|
||||
* Gasless transactions [ApiConfig]
|
||||
*/
|
||||
internal class GaslessTxService(
|
||||
private val authProvider: AuthProvider,
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
private val appInfoProvider: AppInfoProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createProdEnvironment(),
|
||||
createDevEnvironment(),
|
||||
)
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE,
|
||||
DEBUG_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = PROD_BASE_URL,
|
||||
headers = createHeaders(ApiEnvironment.PROD),
|
||||
)
|
||||
|
||||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = DEV_BASE_URL,
|
||||
headers = createHeaders(ApiEnvironment.DEV),
|
||||
)
|
||||
|
||||
private fun createHeaders(environment: ApiEnvironment) = buildMap {
|
||||
putAll(RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
|
||||
put(
|
||||
key = "Authorization",
|
||||
value = ProviderSuspend {
|
||||
"Bearer ${authProvider.getGaslessServiceApiKey(Provider { environment }).invoke()}"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val PROD_BASE_URL = "https://gasless.tangem.org/"
|
||||
private const val DEV_BASE_URL = "[REDACTED_ENV_URL]"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,12 +4,16 @@ import com.tangem.datasource.BuildConfig
|
|||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
/**
|
||||
* News [ApiConfig]
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class News(
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
private val appInfoProvider: AppInfoProvider,
|
||||
private val authProvider: AuthProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
|
|
@ -52,6 +56,7 @@ internal class News(
|
|||
apiEnvironment = Provider { environment },
|
||||
).values,
|
||||
)
|
||||
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ internal class P2PEthPool(
|
|||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||
else -> if (P2PStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
|
||||
else -> if (P2PEthPoolStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
internal class TangemPay(
|
||||
internal sealed class TangemPay(
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
private val environmentConfigStorage: EnvironmentConfigStorage,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
|
@ -16,6 +18,8 @@ internal class TangemPay(
|
|||
createProdEnvironment(),
|
||||
)
|
||||
|
||||
protected abstract fun getBaseUrl(apiEnvironment: ApiEnvironment): String
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||
|
|
@ -31,24 +35,75 @@ internal class TangemPay(
|
|||
|
||||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "https://api.dev.us.paera.com/bff-v2/",
|
||||
headers = createHeaders(),
|
||||
baseUrl = getBaseUrl(ApiEnvironment.DEV),
|
||||
headers = createHeaders(ApiEnvironment.DEV),
|
||||
)
|
||||
|
||||
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
baseUrl = getBaseUrl(ApiEnvironment.MOCK),
|
||||
headers = createHeaders(ApiEnvironment.MOCK),
|
||||
)
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.us.paera.com/bff-v2/",
|
||||
headers = createHeaders(),
|
||||
baseUrl = getBaseUrl(ApiEnvironment.PROD),
|
||||
headers = createHeaders(ApiEnvironment.PROD),
|
||||
)
|
||||
|
||||
private fun createHeaders() = mapOf(
|
||||
private fun createHeaders(apiEnvironment: ApiEnvironment) = mapOf(
|
||||
"version" to ProviderSuspend { appVersionProvider.versionName },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { getBffStaticToken(apiEnvironment) },
|
||||
)
|
||||
|
||||
private fun getBffStaticToken(apiEnvironment: ApiEnvironment): String {
|
||||
return when (apiEnvironment) {
|
||||
ApiEnvironment.MOCK,
|
||||
ApiEnvironment.DEV,
|
||||
-> environmentConfigStorage.getConfigSync().bffStaticTokenDev
|
||||
ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().bffStaticToken
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.STAGE_2,
|
||||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
-> null
|
||||
} ?: error("BffStaticToken is not provided for $apiEnvironment")
|
||||
}
|
||||
|
||||
class Bff(
|
||||
appVersionProvider: AppVersionProvider,
|
||||
environmentConfigStorage: EnvironmentConfigStorage,
|
||||
) : TangemPay(appVersionProvider, environmentConfigStorage) {
|
||||
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
|
||||
return when (apiEnvironment) {
|
||||
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/bff-v2/"
|
||||
ApiEnvironment.MOCK -> "[REDACTED_ENV_URL]"
|
||||
ApiEnvironment.PROD -> "https://api.us.paera.com/bff-v2/"
|
||||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.STAGE_2,
|
||||
-> error("Unknown environment: $apiEnvironment")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Auth(
|
||||
appVersionProvider: AppVersionProvider,
|
||||
environmentConfigStorage: EnvironmentConfigStorage,
|
||||
) : TangemPay(appVersionProvider, environmentConfigStorage) {
|
||||
override fun getBaseUrl(apiEnvironment: ApiEnvironment): String {
|
||||
return when (apiEnvironment) {
|
||||
ApiEnvironment.DEV -> "https://api.dev.us.paera.com/"
|
||||
ApiEnvironment.MOCK -> "[REDACTED_ENV_URL]"
|
||||
ApiEnvironment.PROD -> "https://api.us.paera.com/"
|
||||
ApiEnvironment.DEV_2,
|
||||
ApiEnvironment.DEV_3,
|
||||
ApiEnvironment.STAGE,
|
||||
ApiEnvironment.STAGE_2,
|
||||
-> error("Unknown environment: $apiEnvironment")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
||||
internal class TangemPayAuth(
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
) : ApiConfig() {
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs = listOf(
|
||||
createDevEnvironment(),
|
||||
createMockedEnvironment(),
|
||||
createProdEnvironment(),
|
||||
)
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV
|
||||
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 = "https://api.dev.us.paera.com/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createMockedEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.us.paera.com/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
private fun createHeaders() = mapOf(
|
||||
"version" to ProviderSuspend { appVersionProvider.versionName },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
)
|
||||
}
|
||||
|
|
@ -2,9 +2,7 @@ package com.tangem.datasource.api.ethpool
|
|||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolDepositRequest
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolUnstakeRequest
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolWithdrawRequest
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolTransactionRequest
|
||||
import com.tangem.datasource.api.ethpool.models.response.*
|
||||
import retrofit2.http.*
|
||||
|
||||
|
|
@ -31,13 +29,13 @@ interface P2PEthPoolApi {
|
|||
* Create unsigned transaction for depositing ETH into a vault.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Deposit parameters (delegator address, vault address, amount)
|
||||
* @param body Transaction parameters (delegator address, vault address, amount)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/deposit")
|
||||
suspend fun createDepositTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolDepositRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolDepositResponse>>
|
||||
@Body body: P2PEthPoolTransactionRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolTransactionResponse>>
|
||||
|
||||
/**
|
||||
* Prepare unstake transaction
|
||||
|
|
@ -45,13 +43,13 @@ interface P2PEthPoolApi {
|
|||
* Create unsigned transaction to initiate unstaking process.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Unstake parameters (staker public key, stake transaction hash)
|
||||
* @param body Transaction parameters (delegator address, vault address, amount)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/unstake")
|
||||
suspend fun createUnstakeTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolUnstakeRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolUnstakeResponse>>
|
||||
@Body body: P2PEthPoolTransactionRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolTransactionResponse>>
|
||||
|
||||
/**
|
||||
* Prepare withdrawal transaction
|
||||
|
|
@ -59,13 +57,13 @@ interface P2PEthPoolApi {
|
|||
* Create unsigned transaction to withdraw available funds from exit queue.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Withdrawal parameters (staker address)
|
||||
* @param body Transaction parameters (delegator address, vault address, amount)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/withdraw")
|
||||
suspend fun createWithdrawTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolWithdrawRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolWithdrawResponse>>
|
||||
@Body body: P2PEthPoolTransactionRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolTransactionResponse>>
|
||||
|
||||
/**
|
||||
* Broadcast signed transaction
|
||||
|
|
@ -96,22 +94,4 @@ interface P2PEthPoolApi {
|
|||
@Path("delegatorAddress") delegatorAddress: String,
|
||||
@Path("vaultAddress") vaultAddress: String,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolAccountResponse>>
|
||||
|
||||
/**
|
||||
* Get rewards history
|
||||
*
|
||||
* Retrieve historical rewards data for a specific account and vault.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param delegatorAddress Account address that initiated staking
|
||||
* @param vaultAddress Ethereum address of the vault
|
||||
* @param period Optional period filter (30, 60, or 90 days)
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards")
|
||||
suspend fun getRewards(
|
||||
@Path("network") network: String,
|
||||
@Path("delegatorAddress") delegatorAddress: String,
|
||||
@Path("vaultAddress") vaultAddress: String,
|
||||
@Query("period") period: Int? = null,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolRewardsResponse>>
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for creating deposit transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/staking/deposit
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolDepositRequest(
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "amount")
|
||||
val amount: Double,
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Unified request body for creating staking transactions (deposit, unstake, withdraw)
|
||||
*
|
||||
* Used in:
|
||||
* - POST /api/v1/staking/pool/{network}/staking/deposit
|
||||
* - POST /api/v1/staking/pool/{network}/staking/unstake
|
||||
* - POST /api/v1/staking/pool/{network}/staking/withdraw
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolTransactionRequest(
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "amount")
|
||||
val amount: BigDecimal,
|
||||
)
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for creating unstake transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/staking/unstake
|
||||
*
|
||||
* Note: Documentation seems to contain Bitcoin-related fields (possibly copy-paste error).
|
||||
* Using as-is per specification.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolUnstakeRequest(
|
||||
@Json(name = "stakerPublicKey")
|
||||
val stakerPublicKey: String,
|
||||
@Json(name = "stakeTransactionHash")
|
||||
val stakeTransactionHash: String,
|
||||
)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for creating withdrawal transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/staking/withdraw
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolWithdrawRequest(
|
||||
@Json(name = "stakerAddress")
|
||||
val stakerAddress: String,
|
||||
)
|
||||
|
|
@ -34,7 +34,7 @@ data class P2PEthPoolStakeDTO(
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolExitQueueDTO(
|
||||
@Json(name = "total")
|
||||
val total: Double,
|
||||
val total: BigDecimal,
|
||||
@Json(name = "requests")
|
||||
val requests: List<P2PEthPoolExitRequestDTO>,
|
||||
)
|
||||
|
|
@ -44,11 +44,11 @@ data class P2PEthPoolExitRequestDTO(
|
|||
@Json(name = "ticket")
|
||||
val ticket: String,
|
||||
@Json(name = "totalAssets")
|
||||
val totalAssets: Double,
|
||||
val totalAssets: BigDecimal,
|
||||
@Json(name = "timestamp")
|
||||
val timestamp: Long,
|
||||
@Json(name = "withdrawalTimestamp")
|
||||
val withdrawalTimestamp: Long,
|
||||
val withdrawalTimestamp: Long?,
|
||||
@Json(name = "isClaimable")
|
||||
val isClaimable: Boolean,
|
||||
)
|
||||
|
|
@ -29,7 +29,7 @@ data class P2PEthPoolBroadcastResponse(
|
|||
)
|
||||
|
||||
/**
|
||||
* Transaction status from P2P API
|
||||
* Transaction status from P2PEthPool API
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class P2PEthPoolTxStatusDTO {
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* Response for POST /api/v1/staking/pool/{network}/staking/deposit
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolDepositResponse(
|
||||
@Json(name = "amount")
|
||||
val amount: Double,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "unsignedTransaction")
|
||||
val unsignedTransaction: P2PEthPoolUnsignedTxDTO,
|
||||
@Json(name = "createdAt")
|
||||
val createdAt: DateTime,
|
||||
)
|
||||
|
|
@ -4,9 +4,9 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Error response structure for P2P.org API
|
||||
* Error response structure for P2P.org eth pooled API
|
||||
*
|
||||
* All P2P API endpoints return errors in this format
|
||||
* All P2PEthPool API endpoints return errors in this format
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolErrorResponse(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
|
|||
/**
|
||||
* Unified response wrapper for all P2P.org API responses
|
||||
*
|
||||
* All P2P API endpoints return responses in this format:
|
||||
* All P2PEthPool API endpoints return responses in this format:
|
||||
* ```json
|
||||
* {
|
||||
* "error": null | { code, message, name, errors },
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Response for GET /api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolRewardsResponse(
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "rewards")
|
||||
val rewards: List<P2PEthPoolRewardDTO>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolRewardDTO(
|
||||
@Json(name = "date")
|
||||
val date: DateTime,
|
||||
@Json(name = "apy")
|
||||
val apy: Double,
|
||||
@Json(name = "balance")
|
||||
val balance: BigDecimal,
|
||||
@Json(name = "rewards")
|
||||
val rewards: BigDecimal,
|
||||
)
|
||||
|
|
@ -3,14 +3,20 @@ package com.tangem.datasource.api.ethpool.models.response
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Response for POST /api/v1/staking/pool/{network}/staking/withdraw
|
||||
* Unified response for staking transactions (deposit, unstake, withdraw)
|
||||
*
|
||||
* Response for:
|
||||
* - POST /api/v1/staking/pool/{network}/staking/deposit
|
||||
* - POST /api/v1/staking/pool/{network}/staking/unstake
|
||||
* - POST /api/v1/staking/pool/{network}/staking/withdraw
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolWithdrawResponse(
|
||||
data class P2PEthPoolTransactionResponse(
|
||||
@Json(name = "amount")
|
||||
val amount: Double,
|
||||
val amount: BigDecimal,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "delegatorAddress")
|
||||
|
|
@ -20,5 +26,5 @@ data class P2PEthPoolWithdrawResponse(
|
|||
@Json(name = "createdAt")
|
||||
val createdAt: DateTime,
|
||||
@Json(name = "tickets")
|
||||
val tickets: List<String>,
|
||||
val tickets: List<String>? = null,
|
||||
)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response for POST /api/v1/staking/pool/{network}/staking/unstake
|
||||
*
|
||||
* Note: Contains Bitcoin-related fields (likely documentation error).
|
||||
* Using as-is per specification.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolUnstakeResponse(
|
||||
@Json(name = "stakerPublicKey")
|
||||
val stakerPublicKey: String,
|
||||
@Json(name = "stakeTransactionHash")
|
||||
val stakeTransactionHash: String,
|
||||
@Json(name = "unstakeTransactionHex")
|
||||
val unstakeTransactionHex: String, // unsigned
|
||||
@Json(name = "unstakeFee")
|
||||
val unstakeFee: Double,
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.ethpool.models.response
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Response for GET /api/v1/staking/pool/{network}/vaults
|
||||
|
|
@ -15,7 +16,7 @@ data class P2PEthPoolVaultsResponse(
|
|||
)
|
||||
|
||||
/**
|
||||
* Network identifier in P2P API
|
||||
* Network identifier in P2PEthPool API
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class P2PEthPoolNetworkDTO {
|
||||
|
|
@ -33,15 +34,15 @@ data class P2PEthPoolVaultDTO(
|
|||
@Json(name = "displayName")
|
||||
val displayName: String,
|
||||
@Json(name = "apy")
|
||||
val apy: Double,
|
||||
val apy: BigDecimal,
|
||||
@Json(name = "baseApy")
|
||||
val baseApy: Double,
|
||||
val baseApy: BigDecimal,
|
||||
@Json(name = "capacity")
|
||||
val capacity: Double,
|
||||
val capacity: BigDecimal,
|
||||
@Json(name = "totalAssets")
|
||||
val totalAssets: Double,
|
||||
val totalAssets: BigDecimal,
|
||||
@Json(name = "feePercent")
|
||||
val feePercent: Double,
|
||||
val feePercent: BigDecimal,
|
||||
@Json(name = "isPrivate")
|
||||
val isPrivate: Boolean,
|
||||
@Json(name = "isGenesis")
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ data class ExchangeProvider(
|
|||
|
||||
@Json(name = "slippage")
|
||||
val slippage: BigDecimal?,
|
||||
|
||||
@Json(name = "exchangeOnlyWithinSingleAddress")
|
||||
val isExchangeOnlyWithinSingleAddress: Boolean = false,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.datasource.api.gasless
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.gasless.models.*
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface GaslessTxServiceApi {
|
||||
|
||||
@GET("api/v1/tokens")
|
||||
suspend fun getSupportedTokens(): ApiResponse<GaslessServiceResponse<GaslessSupportedTokens>>
|
||||
|
||||
@POST("api/v1/transaction/sign")
|
||||
suspend fun signGaslessTransaction(
|
||||
@Body transaction: GaslessTransactionRequest,
|
||||
): ApiResponse<GaslessServiceResponse<GaslessSignedTransactionResultDTO>>
|
||||
|
||||
@GET("api/v1/config/fee-recipient")
|
||||
suspend fun getFeeRecipient(): ApiResponse<GaslessServiceResponse<GaslessFeeRecipient>>
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.gasless.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessFeeRecipient(
|
||||
@Json(name = "feeRecipientAddress") val address: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.api.gasless.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessServiceResponse<T>(
|
||||
@Json(name = "result") val result: T,
|
||||
@Json(name = "success") val isSuccess: Boolean,
|
||||
@Json(name = "timestamp") val timestamp: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.datasource.api.gasless.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Result of gasless transaction signing.
|
||||
* Contains the signed transaction data and gas parameters.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessSignedTransactionResultDTO(
|
||||
@Json(name = "txHash")
|
||||
val txHash: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.gasless.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessSupportedTokens(
|
||||
@Json(name = "tokens") val tokens: List<GaslessTokenDTO>,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.datasource.api.gasless.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessTokenDTO(
|
||||
@Json(name = "tokenAddress") val tokenAddress: String,
|
||||
@Json(name = "tokenSymbol") val tokenSymbol: String,
|
||||
@Json(name = "tokenName") val tokenName: String,
|
||||
@Json(name = "decimals") val decimals: Int,
|
||||
@Json(name = "chainId") val chainId: Int,
|
||||
@Json(name = "chain") val chain: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.datasource.api.gasless.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for gasless transaction submission.
|
||||
* Represents complete transaction with fee delegation metadata.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessTransactionRequest(
|
||||
@Json(name = "gaslessTransaction")
|
||||
val gaslessTransaction: GaslessTransactionData,
|
||||
|
||||
@Json(name = "signature")
|
||||
val signature: String,
|
||||
|
||||
@Json(name = "userAddress")
|
||||
val userAddress: String,
|
||||
|
||||
@Json(name = "chainId")
|
||||
val chainId: Int,
|
||||
|
||||
@Json(name = "eip7702auth")
|
||||
val eip7702Auth: Eip7702AuthorizationDTO? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessTransactionData(
|
||||
@Json(name = "transaction")
|
||||
val transaction: TransactionData,
|
||||
|
||||
@Json(name = "fee")
|
||||
val fee: FeeData,
|
||||
|
||||
@Json(name = "nonce")
|
||||
val nonce: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TransactionData(
|
||||
@Json(name = "to")
|
||||
val to: String,
|
||||
|
||||
@Json(name = "value")
|
||||
val value: String,
|
||||
|
||||
@Json(name = "data")
|
||||
val data: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FeeData(
|
||||
@Json(name = "feeToken")
|
||||
val feeToken: String,
|
||||
|
||||
@Json(name = "maxTokenFee")
|
||||
val maxTokenFee: String,
|
||||
|
||||
@Json(name = "coinPriceInToken")
|
||||
val coinPriceInToken: String,
|
||||
|
||||
@Json(name = "feeTransferGasLimit")
|
||||
val feeTransferGasLimit: String,
|
||||
|
||||
@Json(name = "baseGas")
|
||||
val baseGas: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* EIP-7702 authorization for account abstraction.
|
||||
* Optional field, used only when EOA delegation is required.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Eip7702AuthorizationDTO(
|
||||
@Json(name = "chainId")
|
||||
val chainId: Int,
|
||||
|
||||
@Json(name = "address")
|
||||
val address: String,
|
||||
|
||||
@Json(name = "nonce")
|
||||
val nonce: String,
|
||||
|
||||
@Json(name = "yParity")
|
||||
val yParity: Int,
|
||||
|
||||
@Json(name = "r")
|
||||
val r: String,
|
||||
|
||||
@Json(name = "s")
|
||||
val s: String,
|
||||
)
|
||||
|
|
@ -27,15 +27,4 @@ data class NewsRelatedTokenDto(
|
|||
@Json(name = "id") val id: String,
|
||||
@Json(name = "symbol") val symbol: String,
|
||||
@Json(name = "name") val name: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsOriginalArticleDto(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "title") val title: String,
|
||||
@Json(name = "sourceName") val sourceName: String,
|
||||
@Json(name = "language") val language: String,
|
||||
@Json(name = "publishedAt") val publishedAt: String,
|
||||
@Json(name = "url") val url: String,
|
||||
@Json(name = "imageUrl") val imageUrl: String? = null,
|
||||
)
|
||||
|
|
@ -17,4 +17,21 @@ data class NewsDetailsResponse(
|
|||
@Json(name = "shortContent") val shortContent: String,
|
||||
@Json(name = "content") val content: String,
|
||||
@Json(name = "originalArticles") val originalArticles: List<NewsOriginalArticleDto>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsOriginalArticleDto(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "title") val title: String,
|
||||
@Json(name = "source") val source: Source,
|
||||
@Json(name = "language") val language: String,
|
||||
@Json(name = "publishedAt") val publishedAt: String,
|
||||
@Json(name = "url") val url: String,
|
||||
@Json(name = "imageUrl") val imageUrl: String? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Source(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "name") val name: String,
|
||||
)
|
||||
|
|
@ -25,7 +25,6 @@ interface TangemPayApi {
|
|||
|
||||
@GET("v1/customer/wallets/{customer_wallet_id}")
|
||||
suspend fun checkCustomerWalletId(
|
||||
@Header("X-API-KEY") authHeader: String,
|
||||
@Path("customer_wallet_id") customerWalletId: String,
|
||||
): ApiResponse<CheckCustomerWalletResponse>
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,15 @@ internal object ApiConfigsModule {
|
|||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideNewsConfig(authProvider: AuthProvider): ApiConfig = News(authProvider = authProvider)
|
||||
fun provideNewsConfig(
|
||||
appVersionProvider: AppVersionProvider,
|
||||
authProvider: AuthProvider,
|
||||
appInfoProvider: AppInfoProvider,
|
||||
): ApiConfig = News(
|
||||
appVersionProvider = appVersionProvider,
|
||||
appInfoProvider = appInfoProvider,
|
||||
authProvider = authProvider,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
|
|
@ -78,13 +86,17 @@ internal object ApiConfigsModule {
|
|||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPay(appVersionProvider)
|
||||
fun provideTangemPayBffConfig(
|
||||
appVersionProvider: AppVersionProvider,
|
||||
environmentConfigStorage: EnvironmentConfigStorage,
|
||||
): ApiConfig = TangemPay.Bff(appVersionProvider, environmentConfigStorage)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemPayAuthConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemPayAuth(
|
||||
appVersionProvider,
|
||||
)
|
||||
fun provideTangemPayAuthConfig(
|
||||
appVersionProvider: AppVersionProvider,
|
||||
environmentConfigStorage: EnvironmentConfigStorage,
|
||||
): ApiConfig = TangemPay.Auth(appVersionProvider, environmentConfigStorage)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
|
|
@ -97,4 +109,18 @@ internal object ApiConfigsModule {
|
|||
fun provideMoonPayConfig(): ApiConfig {
|
||||
return MoonPay()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideGaslessServiceConfig(
|
||||
appVersionProvider: AppVersionProvider,
|
||||
authProvider: AuthProvider,
|
||||
appInfoProvider: AppInfoProvider,
|
||||
): ApiConfig {
|
||||
return GaslessTxService(
|
||||
authProvider = authProvider,
|
||||
appVersionProvider = appVersionProvider,
|
||||
appInfoProvider = appInfoProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import com.tangem.datasource.api.moonpay.MoonPayApi
|
|||
import com.tangem.datasource.api.news.NewsApi
|
||||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.gasless.GaslessTxServiceApi
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
|
|
@ -36,8 +37,11 @@ import javax.inject.Singleton
|
|||
internal object NetworkModule {
|
||||
|
||||
private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L
|
||||
private const val TANGEM_GASLESS_SERVICE_TIMEOUT_SECONDS = 60L
|
||||
private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L
|
||||
|
||||
private const val P2P_ETH_POOL_API_TIMEOUT_SECONDS = 60L
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApiConfigManager(
|
||||
|
|
@ -82,6 +86,12 @@ internal object NetworkModule {
|
|||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.P2PEthPool,
|
||||
applyTimeoutAnnotations = false,
|
||||
timeouts = Timeouts(
|
||||
callTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS,
|
||||
connectTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS,
|
||||
readTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS,
|
||||
writeTimeoutSeconds = P2P_ETH_POOL_API_TIMEOUT_SECONDS,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -180,4 +190,19 @@ internal object NetworkModule {
|
|||
applyTimeoutAnnotations = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGaslessTxServiceApi(retrofitApiBuilder: RetrofitApiBuilder): GaslessTxServiceApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.GaslessTxService,
|
||||
applyTimeoutAnnotations = false,
|
||||
timeouts = Timeouts(
|
||||
callTimeoutSeconds = TANGEM_GASLESS_SERVICE_TIMEOUT_SECONDS,
|
||||
connectTimeoutSeconds = TANGEM_GASLESS_SERVICE_TIMEOUT_SECONDS,
|
||||
readTimeoutSeconds = TANGEM_GASLESS_SERVICE_TIMEOUT_SECONDS,
|
||||
writeTimeoutSeconds = TANGEM_GASLESS_SERVICE_TIMEOUT_SECONDS,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,12 @@ package com.tangem.datasource.di
|
|||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.news.details.DefaultNewsDetailsStore
|
||||
import com.tangem.datasource.local.news.details.NewsDetailsStore
|
||||
import com.tangem.datasource.local.news.liked.DefaultNewsLikedStore
|
||||
import com.tangem.datasource.local.news.liked.NewsLikedStore
|
||||
import com.tangem.datasource.local.news.trending.DefaultTrendingNewsStore
|
||||
import com.tangem.datasource.local.news.trending.TrendingNewsStore
|
||||
import com.tangem.datasource.local.news.viewed.DefaultNewsViewedStore
|
||||
import com.tangem.datasource.local.news.viewed.NewsViewedStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -26,4 +30,16 @@ internal object NewsStoreModule {
|
|||
fun provideTrendingNewsStore(): TrendingNewsStore {
|
||||
return DefaultTrendingNewsStore(store = RuntimeSharedStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNewsViewedStore(): NewsViewedStore {
|
||||
return DefaultNewsViewedStore(store = RuntimeSharedStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNewsLikedStore(): NewsLikedStore {
|
||||
return DefaultNewsLikedStore(store = RuntimeSharedStore())
|
||||
}
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ internal object StakingStoreModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PBalancesPersistenceStore(
|
||||
fun provideP2PEthPoolBalancesPersistenceStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -91,7 +91,7 @@ internal object StakingStoreModule {
|
|||
types = mapWithStringKeyTypes(valueTypes = setTypes<P2PEthPoolAccountResponse>()),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "p2p_balances") },
|
||||
produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_balances") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,4 +25,7 @@ data class EnvironmentConfig(
|
|||
val yieldModuleApiKey: String? = null,
|
||||
val yieldModuleApiKeyDev: String? = null,
|
||||
val bffStaticToken: String? = null,
|
||||
val bffStaticTokenDev: String? = null,
|
||||
val gaslessTxApiKeyDev: String? = null,
|
||||
val gaslessTxApiKey: String? = null,
|
||||
)
|
||||
|
|
@ -33,6 +33,9 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
|
|||
yieldModuleApiKey = value.yieldModuleApiKey,
|
||||
yieldModuleApiKeyDev = value.yieldModuleApiKeyDev,
|
||||
bffStaticToken = value.bffStaticToken,
|
||||
bffStaticTokenDev = value.bffStaticTokenDev,
|
||||
gaslessTxApiKeyDev = value.gaslessTxApiKeyDev,
|
||||
gaslessTxApiKey = value.gaslessTxApiKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,9 @@ class EnvironmentConfigModel(
|
|||
@Json(name = "blinkApiKey") val blinkApiKey: String?,
|
||||
@Json(name = "tatumApiKey") val tatumApiKey: String?,
|
||||
@Json(name = "bffStaticToken") val bffStaticToken: String?,
|
||||
@Json(name = "bffStaticTokenDev") val bffStaticTokenDev: String?,
|
||||
@Json(name = "gaslessTxApiKeyDev") val gaslessTxApiKeyDev: String?,
|
||||
@Json(name = "gaslessTxApiKey") val gaslessTxApiKey: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.datasource.local.news.liked
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
private typealias NewsLikedCache = Map<Int, Boolean>
|
||||
|
||||
internal class DefaultNewsLikedStore(
|
||||
private val store: RuntimeSharedStore<NewsLikedCache>,
|
||||
) : NewsLikedStore {
|
||||
|
||||
override fun getAll(): Flow<Map<Int, Boolean>> {
|
||||
return store.get().onStart { emit(emptyMap()) }
|
||||
}
|
||||
|
||||
override suspend fun getSync(): Map<Int, Boolean> {
|
||||
return store.getSyncOrNull().orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun updateLiked(articleIds: Collection<Int>, liked: Boolean) {
|
||||
if (articleIds.isEmpty()) return
|
||||
|
||||
store.update(emptyMap()) { current ->
|
||||
val updated = current.toMutableMap()
|
||||
articleIds.forEach { id ->
|
||||
updated[id] = liked
|
||||
}
|
||||
updated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.datasource.local.news.liked
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Store for news liked flags (runtime only).
|
||||
*/
|
||||
interface NewsLikedStore {
|
||||
|
||||
/**
|
||||
* Observes all liked flags.
|
||||
*/
|
||||
fun getAll(): Flow<Map<Int, Boolean>>
|
||||
|
||||
/**
|
||||
* Gets liked flags synchronously (returns empty map if no data).
|
||||
*/
|
||||
suspend fun getSync(): Map<Int, Boolean>
|
||||
|
||||
/**
|
||||
* Updates liked flags for provided article ids.
|
||||
*/
|
||||
suspend fun updateLiked(articleIds: Collection<Int>, liked: Boolean)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.datasource.local.news.viewed
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
|
||||
private typealias NewsViewedCache = Map<Int, Boolean>
|
||||
|
||||
internal class DefaultNewsViewedStore(
|
||||
private val store: RuntimeSharedStore<NewsViewedCache>,
|
||||
) : NewsViewedStore {
|
||||
|
||||
override fun getAll(): Flow<Map<Int, Boolean>> {
|
||||
return store.get().onStart { emit(emptyMap()) }
|
||||
}
|
||||
|
||||
override suspend fun getSync(): Map<Int, Boolean> {
|
||||
return store.getSyncOrNull().orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun updateViewed(articleIds: Collection<Int>, viewed: Boolean) {
|
||||
if (articleIds.isEmpty()) return
|
||||
|
||||
store.update(emptyMap()) { current ->
|
||||
val updated = current.toMutableMap()
|
||||
articleIds.forEach { id ->
|
||||
updated[id] = viewed
|
||||
}
|
||||
updated
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.datasource.local.news.viewed
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Store for news viewed flags (runtime only).
|
||||
*/
|
||||
interface NewsViewedStore {
|
||||
|
||||
/**
|
||||
* Observes all viewed flags.
|
||||
*/
|
||||
fun getAll(): Flow<Map<Int, Boolean>>
|
||||
|
||||
/**
|
||||
* Gets viewed flags synchronously (returns empty map if no data).
|
||||
*/
|
||||
suspend fun getSync(): Map<Int, Boolean>
|
||||
|
||||
/**
|
||||
* Updates viewed flags for provided article ids.
|
||||
*/
|
||||
suspend fun updateViewed(articleIds: Collection<Int>, viewed: Boolean)
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
* (similar to StakingYieldsStore for StakeKit yields)
|
||||
*
|
||||
* Vault is ETH-specific concept for pooled staking.
|
||||
* For other blockchains, P2P may use different structures.
|
||||
* For other blockchains, P2PEthPool may use different structures.
|
||||
*/
|
||||
interface P2PEthPoolVaultsStore {
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ interface P2PEthPoolVaultsStore {
|
|||
suspend fun getSync(): List<P2PEthPoolVault>
|
||||
|
||||
/**
|
||||
* Store vaults from P2P API
|
||||
* Store vaults from P2PEthPool API
|
||||
*/
|
||||
suspend fun store(vaults: List<P2PEthPoolVault>)
|
||||
}
|
||||
|
|
@ -69,12 +69,27 @@ class ApiConfigTest {
|
|||
)
|
||||
}
|
||||
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
|
||||
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = mockk())
|
||||
ApiConfig.ID.TangemPay -> TangemPay.Bff(
|
||||
appVersionProvider = mockk(),
|
||||
environmentConfigStorage = mockk()
|
||||
)
|
||||
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
|
||||
appVersionProvider = mockk(),
|
||||
environmentConfigStorage = mockk()
|
||||
)
|
||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
|
||||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
|
||||
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
|
||||
ApiConfig.ID.TangemPayAuth -> TangemPayAuth(appVersionProvider = mockk())
|
||||
ApiConfig.ID.News -> News(
|
||||
appVersionProvider = mockk(),
|
||||
authProvider = appAuthProvider,
|
||||
appInfoProvider = mockk(),
|
||||
)
|
||||
ApiConfig.ID.GaslessTxService -> GaslessTxService(
|
||||
authProvider = appAuthProvider,
|
||||
appVersionProvider = mockk(),
|
||||
appInfoProvider = mockk(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
|
|||
blockAidApiKey = BLOCK_AID_API_KEY,
|
||||
tangemApiKey = TANGEM_API_KEY,
|
||||
tangemApiKeyDev = TANGEM_API_KEY_DEV,
|
||||
bffStaticToken = TANGEM_PAY_BFF_KEY,
|
||||
bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV,
|
||||
tangemApiKeyStage = TANGEM_API_KEY_STAGE,
|
||||
yieldModuleApiKey = YIELD_MODULE_KEY,
|
||||
yieldModuleApiKeyDev = YIELD_MODULE_KEY_DEV,
|
||||
|
|
@ -33,6 +35,9 @@ internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage {
|
|||
const val BLOCK_AID_API_KEY = "block_aid_api_key"
|
||||
const val TANGEM_API_KEY = "tangem_api_key"
|
||||
const val TANGEM_API_KEY_DEV = "tangem_api_key_dev"
|
||||
const val TANGEM_PAY_BFF_KEY = "tangem_pay_bff_key"
|
||||
const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev"
|
||||
const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key"
|
||||
const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage"
|
||||
const val YIELD_MODULE_KEY = "yield_module_api_key"
|
||||
const val YIELD_MODULE_KEY_DEV = "yield_module_api_key_dev"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_
|
|||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_GASLESS_API_KEY
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_PAY_BFF_KEY_DEV
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
|
|
@ -45,6 +47,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
private val appAuthProvider = mockk<AuthProvider>()
|
||||
private val appInfoProvider = mockk<AppInfoProvider>()
|
||||
private val tangemApiKeyProvider = mockk<ProviderSuspend<String>>()
|
||||
private val tangemGaslessApiKeyProvider = mockk<ProviderSuspend<String>>()
|
||||
|
||||
private lateinit var manager: ProdApiConfigsManager
|
||||
|
||||
|
|
@ -63,6 +66,8 @@ internal class ProdApiConfigsManagerTest {
|
|||
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
|
||||
every { p2pEthPoolAuthProvider.getApiKey() } returns P2P_API_KEY
|
||||
every { appAuthProvider.getApiKey(any()) } returns tangemApiKeyProvider
|
||||
every { appAuthProvider.getGaslessServiceApiKey(any()) } returns tangemGaslessApiKeyProvider
|
||||
coEvery { tangemGaslessApiKeyProvider.invoke() } returns TANGEM_GASLESS_API_KEY
|
||||
coEvery { tangemApiKeyProvider.invoke() } returns TANGEM_API_KEY
|
||||
coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID
|
||||
coEvery { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
|
||||
|
|
@ -111,12 +116,27 @@ internal class ProdApiConfigsManagerTest {
|
|||
)
|
||||
}
|
||||
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
|
||||
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = appVersionProvider)
|
||||
ApiConfig.ID.TangemPay -> TangemPay.Bff(
|
||||
appVersionProvider = appVersionProvider,
|
||||
environmentConfigStorage = environmentConfigStorage,
|
||||
)
|
||||
ApiConfig.ID.TangemPayAuth -> TangemPay.Auth(
|
||||
appVersionProvider = appVersionProvider,
|
||||
environmentConfigStorage = environmentConfigStorage,
|
||||
)
|
||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
|
||||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
|
||||
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
|
||||
ApiConfig.ID.TangemPayAuth -> TangemPayAuth(appVersionProvider = appVersionProvider)
|
||||
ApiConfig.ID.News -> News(
|
||||
appVersionProvider = appVersionProvider,
|
||||
authProvider = appAuthProvider,
|
||||
appInfoProvider = appInfoProvider,
|
||||
)
|
||||
ApiConfig.ID.GaslessTxService -> GaslessTxService(
|
||||
authProvider = appAuthProvider,
|
||||
appVersionProvider = appVersionProvider,
|
||||
appInfoProvider = appInfoProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -128,11 +148,12 @@ internal class ProdApiConfigsManagerTest {
|
|||
ApiConfig.ID.TangemTech -> createTangemTechModel()
|
||||
ApiConfig.ID.StakeKit -> createStakeKitModel()
|
||||
ApiConfig.ID.TangemPay -> createTangemPayModel()
|
||||
ApiConfig.ID.TangemPayAuth -> createTangemPayAuthModel()
|
||||
ApiConfig.ID.BlockAid -> createBlockAidSdkModel()
|
||||
ApiConfig.ID.MoonPay -> createMoonPayModel()
|
||||
ApiConfig.ID.P2PEthPool -> createP2PModel()
|
||||
ApiConfig.ID.News -> createNewsModel()
|
||||
ApiConfig.ID.TangemPayAuth -> createTangemPayAuthModel()
|
||||
ApiConfig.ID.GaslessTxService -> createGaslessTxServiceModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -255,6 +276,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
headers = mapOf(
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -269,6 +291,38 @@ internal class ProdApiConfigsManagerTest {
|
|||
headers = mapOf(
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createGaslessTxServiceModel(): TestModel {
|
||||
val (environment, baseUrl) = when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE,
|
||||
DEBUG_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV to "[REDACTED_ENV_URL]"
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD to "https://gasless.tangem.org/"
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.GaslessTxService,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = environment,
|
||||
baseUrl = baseUrl,
|
||||
headers = mapOf(
|
||||
"Authorization" to ProviderSuspend { "Bearer $TANGEM_GASLESS_API_KEY" },
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "android" },
|
||||
"system_version" to ProviderSuspend { "Android 16" },
|
||||
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
|
||||
"timezone" to ProviderSuspend {
|
||||
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
|
||||
},
|
||||
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -300,7 +354,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
}
|
||||
|
||||
private fun createP2PModel(): TestModel {
|
||||
val (environment, baseUrl) = if (P2PStakingConfig.USE_TESTNET) {
|
||||
val (environment, baseUrl) = if (P2PEthPoolStakingConfig.USE_TESTNET) {
|
||||
ApiEnvironment.DEV to "https://api-test.p2p.org/"
|
||||
} else {
|
||||
ApiEnvironment.PROD to "https://api.p2p.org/"
|
||||
|
|
@ -338,6 +392,14 @@ internal class ProdApiConfigsManagerTest {
|
|||
baseUrl = baseUrl,
|
||||
headers = mapOf(
|
||||
"api-key" to ProviderSuspend { TANGEM_API_KEY },
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "android" },
|
||||
"system_version" to ProviderSuspend { "Android 16" },
|
||||
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
|
||||
"timezone" to ProviderSuspend {
|
||||
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()
|
||||
},
|
||||
"device" to ProviderSuspend { "${Build.MANUFACTURER} ${Build.MODEL}".checkHeaderValueOrEmpty() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.core.decompose.utils
|
||||
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Retrieves the current stack of items from the [com.arkivanov.decompose.router.stack.ChildStack]
|
||||
* @return A list representing the current stack of items
|
||||
*/
|
||||
suspend fun <T : Any> StackNavigation<T>.stack(): List<T> {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
this.navigate(
|
||||
transformer = { stack -> stack },
|
||||
onComplete = { newStack, oldStack -> continuation.resume(newStack) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,11 +14,13 @@ sealed class BatchFetchResult<out TData> {
|
|||
* @param data fetched data.
|
||||
* @param empty indicates that data is empty and [BatchListSource] shouldn't create new batch for this result
|
||||
* @param last indicates if this is the last batch for the request.
|
||||
* @param total total number of items available on the server.
|
||||
*/
|
||||
data class Success<TData>(
|
||||
val data: TData,
|
||||
val empty: Boolean,
|
||||
val last: Boolean,
|
||||
val total: Int? = null,
|
||||
) : BatchFetchResult<TData>()
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
private val updateFetcher: BatchUpdateFetcher<TKey, TData, TUpdate>? = null,
|
||||
) : BatchListSource<TKey, TData, TUpdate> {
|
||||
|
||||
override val state = MutableStateFlow(BatchListState<TKey, TData>(emptyList(), PaginationStatus.None))
|
||||
override val state = MutableStateFlow(BatchListState<TKey, TData>(emptyList(), PaginationStatus.None, null))
|
||||
override val updateResults = MutableSharedFlow<Pair<TUpdate, BatchUpdateResult<TKey, TData>>>(
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
|
|
@ -254,6 +254,7 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
} else {
|
||||
PaginationStatus.Paginating(res)
|
||||
},
|
||||
totalCount = res.total,
|
||||
)
|
||||
}
|
||||
is BatchFetchResult.Error -> {
|
||||
|
|
@ -262,6 +263,7 @@ private class DefaultBatchListSource<TKey, TData, TRequestParams : Any, TUpdate>
|
|||
status = PaginationStatus.InitialLoadingError(
|
||||
throwable = res.throwable,
|
||||
),
|
||||
totalCount = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ package com.tangem.pagination
|
|||
*
|
||||
* @property data list of loaded batches.
|
||||
* @property status current status of the pagination.
|
||||
* @property totalCount total number of items available on the server.
|
||||
*
|
||||
* @see BatchListSource
|
||||
*/
|
||||
data class BatchListState<TKey, TData>(
|
||||
val data: List<Batch<TKey, TData>>,
|
||||
val status: PaginationStatus<TData>,
|
||||
val totalCount: Int? = null,
|
||||
)
|
||||
|
|
@ -61,6 +61,16 @@ dependencies {
|
|||
api(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.markdown)
|
||||
implementation(deps.haze) {
|
||||
exclude(module = "activity-compose")
|
||||
exclude(module = "activity")
|
||||
exclude(module = "activity-ktx")
|
||||
}
|
||||
implementation(deps.haze.materials) {
|
||||
exclude(module = "activity-compose")
|
||||
exclude(module = "activity")
|
||||
exclude(module = "activity-ktx")
|
||||
}
|
||||
|
||||
/** Tests */
|
||||
testImplementation(deps.test.junit)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,6 @@
|
|||
<ID>BooleanPropertyNaming:BigDecimalPercentFormat.kt$BigDecimalPercentFormat$val withPercentSign: Boolean = true</ID>
|
||||
<ID>BooleanPropertyNaming:FullScreen.kt$FullScreenLayout$private val focusable: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:TokenItemState.kt$TokenItemState.TitleState.Content$val earnApyIsActive: Boolean = false</ID>
|
||||
<ID>CanBeNonNullable:FooterContainer.kt$footer: TextReference? = null</ID>
|
||||
<ID>CanBeNonNullable:InputRowImageBase.kt$iconRes: Int?</ID>
|
||||
<ID>CanBeNonNullable:InputRowImageInfo.kt$subtitleEndIconRes: Int?</ID>
|
||||
<ID>CanBeNonNullable:SearchBar.kt$keyboardController: SoftwareKeyboardController?</ID>
|
||||
<ID>CanBeNonNullable:TextFields.kt$caption: String? = null</ID>
|
||||
<ID>CanBeNonNullable:TransactionList.kt$txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>?</ID>
|
||||
<ID>CastNullableToNonNullableType:DialogFullScreen.kt$as</ID>
|
||||
<ID>CastNullableToNonNullableType:FullScreen.kt$FullScreenLayout$as</ID>
|
||||
<ID>MultilineLambdaItParameter:Actions.kt${ ActionButtonContent( config = config, text = { textColor -> Text(text = config.text, textColor = textColor) }, modifier = it.padding( start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24, ), ) }</ID>
|
||||
|
|
@ -27,6 +21,5 @@
|
|||
<ID>ReusedModifierInstance:TangemRadioButton.kt$AnimatedVisibility( visible = isSelected, label = "Radio button animation", modifier = modifier .size(TangemTheme.dimens.size24), ) { Icon( painter = painterResource(id = R.drawable.ic_check_circle_24), contentDescription = null, tint = TangemTheme.colors.control.checked, ) }</ID>
|
||||
<ID>ReusedModifierInstance:TokenPrice.kt$Icon( modifier = modifier, painter = painterResource( id = when (animatedType) { PriceChangeType.UP -> R.drawable.ic_arrow_up_8 PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 }, ), tint = when (animatedType) { PriceChangeType.UP -> TangemTheme.colors.icon.accent PriceChangeType.DOWN -> TangemTheme.colors.icon.warning PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive }, contentDescription = null, )</ID>
|
||||
<ID>UnnecessaryEventHandlerParameter:PinTextField.kt$onValueChange: (String) -> Unit</ID>
|
||||
<ID>UnnecessaryEventHandlerParameter:ResizableText.kt$onFontSizeChange: (Float) -> Unit</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.core.ui
|
||||
|
||||
interface DesignFeatureToggles {
|
||||
val isRedesignEnabled: Boolean
|
||||
}
|
||||
|
|
@ -16,4 +16,6 @@ interface UiDependencies {
|
|||
val globalSnackbarHostState: SnackbarHostState
|
||||
|
||||
val eventMessageHandler: EventMessageHandler
|
||||
|
||||
val designFeatureToggles: DesignFeatureToggles
|
||||
}
|
||||
|
|
@ -170,6 +170,8 @@ fun PrimaryButtonIconStart(
|
|||
modifier: Modifier = Modifier,
|
||||
showProgress: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
size: TangemButtonSize = TangemButtonSize.Default,
|
||||
shape: Shape = size.toShape(),
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
|
|
@ -180,6 +182,8 @@ fun PrimaryButtonIconStart(
|
|||
enabled = enabled,
|
||||
showProgress = showProgress,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
size = size,
|
||||
shape = shape,
|
||||
)
|
||||
}
|
||||
// endregion PrimaryButton
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Currency placeholder icon
|
||||
*
|
||||
* @param id currency id
|
||||
* @param modifier modifier
|
||||
*/
|
||||
@Composable
|
||||
fun CurrencyPlaceholderIcon(id: String, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier.background(color = TangemTheme.colors.icon.secondary, shape = CircleShape),
|
||||
) {
|
||||
Text(
|
||||
text = id.firstOrNull()?.titlecase().orEmpty(),
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing4),
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
textAlign = TextAlign.Center,
|
||||
// FIXME("Incorrect typography. Replace with typography from design system")
|
||||
style = TangemTheme.typography.h3.copy(fontWeight = FontWeight.Bold),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
|
||||
@Preview(showBackground = true, heightDp = 40, widthDp = 40)
|
||||
@Preview(showBackground = true, heightDp = 40, widthDp = 40, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_CurrencyPlaceholderIcon() {
|
||||
TangemThemePreview {
|
||||
CurrencyPlaceholderIcon(id = "DAI")
|
||||
}
|
||||
}
|
||||
// endregion preview
|
||||
|
|
@ -70,17 +70,6 @@ fun BasicDialog(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) {
|
||||
TangemDialog(
|
||||
type = DialogType.Message(message),
|
||||
confirmButton = DialogButtonUM(onClick = onDismissDialog),
|
||||
onDismissDialog = onDismissDialog,
|
||||
title = null,
|
||||
dismissButton = null,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog with text field
|
||||
*
|
||||
|
|
@ -124,34 +113,6 @@ fun TextInputDialog(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextInputDialog(
|
||||
fieldValue: String,
|
||||
confirmButton: DialogButtonUM,
|
||||
onDismissDialog: () -> Unit,
|
||||
onValueChange: (String) -> Unit,
|
||||
textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() },
|
||||
title: String? = null,
|
||||
dismissButton: DialogButtonUM? = null,
|
||||
isDismissable: Boolean = true,
|
||||
) {
|
||||
TangemDialog(
|
||||
type = DialogType.SimpleTextInput(
|
||||
value = fieldValue,
|
||||
onValueChange = onValueChange,
|
||||
params = textFieldParams,
|
||||
),
|
||||
confirmButton = confirmButton,
|
||||
onDismissDialog = onDismissDialog,
|
||||
title = title,
|
||||
dismissButton = dismissButton,
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = isDismissable,
|
||||
dismissOnClickOutside = isDismissable,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectorDialog(
|
||||
selectedItemIndex: Int,
|
||||
|
|
@ -441,14 +402,6 @@ private sealed class DialogType {
|
|||
// endregion Defaults
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
private fun SimpleOkDialogPreview() {
|
||||
SimpleOkDialog(
|
||||
message = "All protected passwords will be deleted from the " +
|
||||
"secure storage, you must enter the wallet password to work with the app.",
|
||||
) {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BasicDialogPreview() {
|
||||
BasicDialog(
|
||||
|
|
@ -461,15 +414,6 @@ private fun BasicDialogPreview() {
|
|||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_SimpleOkDialog() {
|
||||
TangemThemePreview {
|
||||
SimpleOkDialogPreview()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.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.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(
|
||||
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,
|
||||
) {
|
||||
// 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<TextLayoutResult?>(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
|
||||
}
|
||||
|
||||
val ellipsisWidth = layoutText.indices.toList()
|
||||
.takeLast(ELLIPSIS_CHARACTERS_COUNT)
|
||||
.let widthLet@{ indices ->
|
||||
// fix this bug: https://issuetracker.google.com/issues/197146630
|
||||
// in this case width is invalid
|
||||
for (i in indices) {
|
||||
val width = textLayoutResult.getBoundingBox(i).width
|
||||
if (width > 0) {
|
||||
return@widthLet width * ELLIPSIS_CHARACTERS_COUNT
|
||||
}
|
||||
}
|
||||
// this should not happen, because
|
||||
// this error occurs only for the last character in the string
|
||||
error("all ellipsis chars have invalid width")
|
||||
}
|
||||
val availableWidth = constraints.maxWidth - ellipsisWidth
|
||||
val startCounter = BoundCounter(text, textLayoutResult) { it }
|
||||
val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it }
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
startCounter.string.trimEnd() + ellipsisText + endCounter.string.reversed().trimStart()
|
||||
}
|
||||
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_CHARACTERS_COUNT = 3
|
||||
private const val ELLIPSIS_CHARACTER = '.'
|
||||
private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "")
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private const val COEFFICIENT = 0.8f
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
fun ResizableText(
|
||||
text: String,
|
||||
fontSizeRange: FontSizeRange,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
) {
|
||||
val fontSizeValue = remember { mutableFloatStateOf(fontSizeRange.max.value) }
|
||||
val readyToDraw = remember { mutableStateOf(false) }
|
||||
|
||||
val textState = remember { mutableStateOf(text) }
|
||||
if (textState.value != text) {
|
||||
readyToDraw.value = false
|
||||
fontSizeValue.floatValue = fontSizeRange.max.value
|
||||
textState.value = text
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() },
|
||||
color = color,
|
||||
fontSize = fontSizeValue.floatValue.sp,
|
||||
overflow = overflow,
|
||||
softWrap = false,
|
||||
maxLines = maxLines,
|
||||
onTextLayout = { textLayoutResult ->
|
||||
if (textLayoutResult.hasVisualOverflow) {
|
||||
val nextFontSizeValue = fontSizeValue.floatValue - fontSizeRange.step.value
|
||||
if (nextFontSizeValue <= fontSizeRange.min.value) {
|
||||
fontSizeValue.floatValue = fontSizeRange.min.value
|
||||
readyToDraw.value = true
|
||||
} else {
|
||||
fontSizeValue.floatValue = nextFontSizeValue * COEFFICIENT
|
||||
}
|
||||
} else {
|
||||
readyToDraw.value = true
|
||||
}
|
||||
},
|
||||
style = style,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Composable function that displays text which can be resized based on its content's overflow.
|
||||
*
|
||||
* This function draws text on the screen and checks if it overflows. If the text overflows,
|
||||
* its font size is reduced recursively until it either fits the available space or reaches a
|
||||
* specified minimum font size.
|
||||
*/
|
||||
@Composable
|
||||
fun ResizableText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
textAlign: TextAlign? = null,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
minFontSize: TextUnit = TextUnit.Unspecified,
|
||||
@FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
|
||||
reduceFactor: Double = 0.9,
|
||||
) {
|
||||
var fontSize by remember { mutableStateOf(style.fontSize) }
|
||||
var isReadyToDraw by remember { mutableStateOf(value = false) }
|
||||
|
||||
Text(
|
||||
modifier = modifier
|
||||
.drawWithContent {
|
||||
if (isReadyToDraw) drawContent()
|
||||
}
|
||||
.wrapContentHeight(),
|
||||
text = text,
|
||||
color = color,
|
||||
fontSize = fontSize,
|
||||
textAlign = textAlign,
|
||||
overflow = overflow,
|
||||
softWrap = softWrap,
|
||||
maxLines = maxLines,
|
||||
style = style,
|
||||
onTextLayout = { result ->
|
||||
fun reduceFontSize() {
|
||||
val reducedFontSize = fontSize * reduceFactor
|
||||
|
||||
if (minFontSize != TextUnit.Unspecified && reducedFontSize <= minFontSize) {
|
||||
fontSize = minFontSize
|
||||
isReadyToDraw = true
|
||||
} else {
|
||||
fontSize = reducedFontSize
|
||||
}
|
||||
}
|
||||
|
||||
if (result.hasVisualOverflow) {
|
||||
reduceFontSize()
|
||||
} else {
|
||||
isReadyToDraw = true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResizableText(
|
||||
text: String,
|
||||
fontSizeValue: TextUnit,
|
||||
fontSizeRange: FontSizeRange,
|
||||
onFontSizeChange: (Float) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
) {
|
||||
val readyToDraw = remember { mutableStateOf(false) }
|
||||
|
||||
val textState = remember { mutableStateOf(text) }
|
||||
if (textState.value != text) {
|
||||
readyToDraw.value = false
|
||||
onFontSizeChange(fontSizeRange.max.value)
|
||||
textState.value = text
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() },
|
||||
color = color,
|
||||
fontSize = fontSizeValue.value.sp,
|
||||
overflow = overflow,
|
||||
softWrap = false,
|
||||
maxLines = maxLines,
|
||||
onTextLayout = { result ->
|
||||
if (result.hasVisualOverflow) {
|
||||
val nextFontSizeValue = fontSizeValue.value - fontSizeRange.step.value
|
||||
if (nextFontSizeValue <= fontSizeRange.min.value) {
|
||||
onFontSizeChange(fontSizeRange.min.value)
|
||||
readyToDraw.value = true
|
||||
} else {
|
||||
val newSizeValue = nextFontSizeValue * COEFFICIENT
|
||||
onFontSizeChange(newSizeValue)
|
||||
}
|
||||
} else {
|
||||
readyToDraw.value = true
|
||||
}
|
||||
},
|
||||
style = style,
|
||||
)
|
||||
}
|
||||
|
||||
data class FontSizeRange(
|
||||
val min: TextUnit,
|
||||
val max: TextUnit,
|
||||
val step: TextUnit = DEFAULT_TEXT_STEP,
|
||||
) {
|
||||
init {
|
||||
require(min < max) { "min should be less than max, $this" }
|
||||
require(step.value > 0) { "step should be greater than 0, $this" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_TEXT_STEP = 1.sp
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
fun AppBarWithBackButtonAndIcon(
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
backButtonEnabled: Boolean = true,
|
||||
endButtonEnabled: Boolean = true,
|
||||
text: String? = null,
|
||||
subtitle: String? = null,
|
||||
@DrawableRes backIconRes: Int? = null,
|
||||
|
|
@ -30,11 +32,13 @@ fun AppBarWithBackButtonAndIcon(
|
|||
startButton = TopAppBarButtonUM.Icon(
|
||||
iconRes = backIconRes ?: R.drawable.ic_back_24,
|
||||
onClicked = onBackClick,
|
||||
isEnabled = backButtonEnabled,
|
||||
),
|
||||
endButton = if (iconRes != null && onIconClick != null) {
|
||||
TopAppBarButtonUM.Icon(
|
||||
iconRes = iconRes,
|
||||
onClicked = onIconClick,
|
||||
isEnabled = endButtonEnabled,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ private fun getTextColorByType(type: AuditLabelUM.Type): Color {
|
|||
AuditLabelUM.Type.Warning -> TangemTheme.colors.text.attention
|
||||
AuditLabelUM.Type.Permit -> TangemTheme.colors.text.accent
|
||||
AuditLabelUM.Type.Info -> TangemTheme.colors.text.primary2
|
||||
AuditLabelUM.Type.General -> TangemTheme.colors.text.secondary
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +74,7 @@ private fun getBackgroundColorByType(type: AuditLabelUM.Type): Color {
|
|||
AuditLabelUM.Type.Warning -> TangemTheme.colors.text.attention.copy(alpha = 0.1f)
|
||||
AuditLabelUM.Type.Permit -> TangemTheme.colors.text.accent.copy(alpha = 0.1f)
|
||||
AuditLabelUM.Type.Info -> TangemTheme.colors.text.accent
|
||||
AuditLabelUM.Type.General -> TangemTheme.colors.control.default
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,6 +85,7 @@ private fun getPaddingByType(type: AuditLabelUM.Type): PaddingValues {
|
|||
AuditLabelUM.Type.Prohibition,
|
||||
AuditLabelUM.Type.Warning,
|
||||
AuditLabelUM.Type.Permit,
|
||||
AuditLabelUM.Type.General,
|
||||
-> PaddingValues(horizontal = 4.dp)
|
||||
AuditLabelUM.Type.Info -> PaddingValues(horizontal = 6.dp, vertical = 1.dp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,5 +25,8 @@ data class AuditLabelUM(val text: TextReference, val type: Type) {
|
|||
|
||||
/** Blue with white text. Like, "best rate" */
|
||||
Info,
|
||||
|
||||
/** Gray. Like, "info" */
|
||||
General,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +1,25 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.internal
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ModalBottomSheetWithBackHandling(
|
||||
onDismissRequest: () -> Unit,
|
||||
onBack: (() -> Unit),
|
||||
modifier: Modifier = Modifier,
|
||||
onBack: (() -> Unit)? = null,
|
||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
|
||||
shape: Shape = BottomSheetDefaults.ExpandedShape,
|
||||
|
|
@ -38,28 +31,9 @@ fun ModalBottomSheetWithBackHandling(
|
|||
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
BackHandler(enabled = sheetState.targetValue != SheetValue.Hidden) {
|
||||
onBack()
|
||||
}
|
||||
|
||||
val requester = remember { FocusRequester() }
|
||||
val backPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier
|
||||
.focusRequester(requester)
|
||||
.focusable()
|
||||
.onPreviewKeyEvent { keyEvent ->
|
||||
if (keyEvent.key == Key.Back &&
|
||||
keyEvent.type == KeyEventType.KeyUp &&
|
||||
!keyEvent.nativeKeyEvent.isCanceled
|
||||
) {
|
||||
backPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed()
|
||||
return@onPreviewKeyEvent true
|
||||
}
|
||||
return@onPreviewKeyEvent false
|
||||
},
|
||||
modifier = modifier,
|
||||
sheetState = sheetState,
|
||||
sheetMaxWidth = sheetMaxWidth,
|
||||
shape = shape,
|
||||
|
|
@ -69,18 +43,17 @@ fun ModalBottomSheetWithBackHandling(
|
|||
scrimColor = scrimColor,
|
||||
dragHandle = dragHandle,
|
||||
contentWindowInsets = contentWindowInsets,
|
||||
properties = ModalBottomSheetDefaults.properties(
|
||||
// Set false otherwise the onPreviewKeyEvent doesn't work at all.
|
||||
// The functionality of shouldDismissOnBackPress is achieved by the BackHandler.
|
||||
shouldDismissOnBackPress = false,
|
||||
properties = ModalBottomSheetProperties(
|
||||
shouldDismissOnBackPress = onBack == null,
|
||||
),
|
||||
content = content,
|
||||
)
|
||||
content = {
|
||||
content()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(timeMillis = 200)
|
||||
requester.requestFocus()
|
||||
}
|
||||
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
|
||||
onBack?.invoke()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
|||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.components.icons.HighlightedIcon
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -109,7 +110,7 @@ private fun ContentContainer(state: MessageBottomSheetUMV2.InfoBlock, modifier:
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = TangemTheme.dimens.spacing8),
|
||||
text = body.resolveReference(),
|
||||
text = body.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -124,7 +125,6 @@ private fun ContentContainer(state: MessageBottomSheetUMV2.InfoBlock, modifier:
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("CanBeNonNullable")
|
||||
@Composable
|
||||
private fun BottomSheetIconContainer(
|
||||
icon: MessageBottomSheetUMV2.Icon?,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -146,7 +145,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheet(
|
|||
sheetState = SheetState(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
initialValue = Expanded,
|
||||
density = LocalDensity.current,
|
||||
positionalThreshold = { 0f },
|
||||
velocityThreshold = { 0f },
|
||||
),
|
||||
onBack = null,
|
||||
bsContent = {
|
||||
|
|
@ -207,32 +207,18 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
|
|||
noinline onBack: (() -> Unit)? = null,
|
||||
noinline bsContent: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
if (onBack != null) {
|
||||
ModalBottomSheetWithBackHandling(
|
||||
modifier = modifier,
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = Color.Transparent,
|
||||
shape = TangemTheme.shapes.roundedCornersLarge,
|
||||
contentWindowInsets = { WindowInsetsZero },
|
||||
onBack = onBack,
|
||||
dragHandle = null,
|
||||
content = bsContent,
|
||||
scrimColor = TangemTheme.colors.overlay.secondary,
|
||||
)
|
||||
} else {
|
||||
ModalBottomSheet(
|
||||
modifier = modifier,
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = Color.Transparent,
|
||||
shape = TangemTheme.shapes.roundedCornersLarge,
|
||||
contentWindowInsets = { WindowInsetsZero },
|
||||
dragHandle = null,
|
||||
content = bsContent,
|
||||
scrimColor = TangemTheme.colors.overlay.secondary,
|
||||
)
|
||||
}
|
||||
ModalBottomSheetWithBackHandling(
|
||||
modifier = modifier,
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = Color.Transparent,
|
||||
shape = TangemTheme.shapes.roundedCornersLarge,
|
||||
contentWindowInsets = { WindowInsetsZero },
|
||||
onBack = onBack,
|
||||
dragHandle = null,
|
||||
content = bsContent,
|
||||
scrimColor = TangemTheme.colors.overlay.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -141,7 +140,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
|
|||
sheetState = SheetState(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
initialValue = Expanded,
|
||||
density = LocalDensity.current,
|
||||
positionalThreshold = { 0f },
|
||||
velocityThreshold = { 0f },
|
||||
),
|
||||
onBack = null,
|
||||
containerColor = containerColor,
|
||||
|
|
@ -247,32 +247,18 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
|
|||
}
|
||||
}
|
||||
|
||||
if (onBack != null) {
|
||||
ModalBottomSheetWithBackHandling(
|
||||
modifier = modifier,
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = Color.Transparent,
|
||||
shape = TangemTheme.shapes.roundedCornersLarge,
|
||||
contentWindowInsets = { WindowInsetsZero },
|
||||
onBack = onBack,
|
||||
dragHandle = null,
|
||||
content = bsContent,
|
||||
scrimColor = TangemTheme.colors.overlay.secondary,
|
||||
)
|
||||
} else {
|
||||
ModalBottomSheet(
|
||||
modifier = modifier,
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = Color.Transparent,
|
||||
shape = TangemTheme.shapes.roundedCornersLarge,
|
||||
contentWindowInsets = { WindowInsetsZero },
|
||||
dragHandle = null,
|
||||
content = bsContent,
|
||||
scrimColor = TangemTheme.colors.overlay.secondary,
|
||||
)
|
||||
}
|
||||
ModalBottomSheetWithBackHandling(
|
||||
modifier = modifier,
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = Color.Transparent,
|
||||
shape = TangemTheme.shapes.roundedCornersLarge,
|
||||
contentWindowInsets = { WindowInsetsZero },
|
||||
onBack = onBack,
|
||||
dragHandle = null,
|
||||
content = bsContent,
|
||||
scrimColor = TangemTheme.colors.overlay.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.core.ui.components.bottomsheets.sheet
|
|||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.SheetValue.Expanded
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
|
|
@ -134,7 +133,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
|
|||
sheetState = SheetState(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
initialValue = Expanded,
|
||||
density = LocalDensity.current,
|
||||
positionalThreshold = { 0f },
|
||||
velocityThreshold = { 0f },
|
||||
),
|
||||
onBack = null,
|
||||
containerColor = containerColor,
|
||||
|
|
@ -181,30 +181,16 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
|
|||
}
|
||||
}
|
||||
|
||||
if (onBack != null) {
|
||||
ModalBottomSheetWithBackHandling(
|
||||
modifier = modifier.statusBarsPadding(),
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = containerColor,
|
||||
shape = TangemTheme.shapes.bottomSheetLarge,
|
||||
contentWindowInsets = { WindowInsetsZero },
|
||||
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
|
||||
onBack = onBack,
|
||||
content = bsContent,
|
||||
scrimColor = TangemTheme.colors.overlay.secondary,
|
||||
)
|
||||
} else {
|
||||
ModalBottomSheet(
|
||||
modifier = modifier.statusBarsPadding(),
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = containerColor,
|
||||
shape = TangemTheme.shapes.bottomSheetLarge,
|
||||
contentWindowInsets = { WindowInsetsZero },
|
||||
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
|
||||
content = bsContent,
|
||||
scrimColor = TangemTheme.colors.overlay.secondary,
|
||||
)
|
||||
}
|
||||
ModalBottomSheetWithBackHandling(
|
||||
modifier = modifier.statusBarsPadding(),
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = containerColor,
|
||||
shape = TangemTheme.shapes.bottomSheetLarge,
|
||||
contentWindowInsets = { WindowInsetsZero },
|
||||
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
|
||||
onBack = onBack,
|
||||
content = bsContent,
|
||||
scrimColor = TangemTheme.colors.overlay.secondary,
|
||||
)
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.TextAutoSize
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
|
|
@ -23,7 +24,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.ResizableText
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.utils.MultipleClickPreventer
|
||||
|
|
@ -75,7 +75,7 @@ fun TangemButton(
|
|||
)
|
||||
},
|
||||
text = {
|
||||
ResizableText(
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.heightIn(MinButtonContentSize, maxContentSize)
|
||||
|
|
@ -86,7 +86,10 @@ fun TangemButton(
|
|||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
minFontSize = 12.sp,
|
||||
autoSize = TextAutoSize.StepBased(
|
||||
minFontSize = 12.sp,
|
||||
maxFontSize = textStyle.fontSize,
|
||||
),
|
||||
)
|
||||
},
|
||||
icon = { iconResId ->
|
||||
|
|
|
|||
122
core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt
Normal file
122
core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package com.tangem.core.ui.components.chip
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.chip.entity.ChipUM
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun Chip(state: ChipUM, modifier: Modifier = Modifier) {
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = if (state.isSelected) {
|
||||
TangemTheme.colors.button.primary
|
||||
} else {
|
||||
TangemTheme.colors.button.secondary
|
||||
},
|
||||
)
|
||||
|
||||
val textColor by animateColorAsState(
|
||||
targetValue = if (state.isSelected) {
|
||||
TangemTheme.colors.text.primary2
|
||||
} else {
|
||||
TangemTheme.colors.text.primary1
|
||||
},
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(color = backgroundColor)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple(),
|
||||
onClick = state.onClick,
|
||||
)
|
||||
.padding(PaddingValues(horizontal = 24.dp, vertical = 8.dp)),
|
||||
) {
|
||||
Text(
|
||||
text = state.text.resolveReference(),
|
||||
style = TangemTheme.typography.button,
|
||||
color = textColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ChipPreview() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Chip(
|
||||
state = ChipUM(
|
||||
id = 0,
|
||||
text = TextReference.Str("All News"),
|
||||
isSelected = true,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
Chip(
|
||||
state = ChipUM(
|
||||
id = 1,
|
||||
text = TextReference.Str("Regulation"),
|
||||
isSelected = false,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
Chip(
|
||||
state = ChipUM(
|
||||
id = 2,
|
||||
text = TextReference.Str("ETFs"),
|
||||
isSelected = false,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
Chip(
|
||||
state = ChipUM(
|
||||
id = 3,
|
||||
text = TextReference.Str("Institutions"),
|
||||
isSelected = false,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.core.ui.components.chip.entity
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
data class ChipUM(
|
||||
val id: Int,
|
||||
val text: TextReference,
|
||||
val isSelected: Boolean = false,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -10,9 +10,8 @@ import androidx.compose.foundation.text.BasicTextField
|
|||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusManager
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -51,16 +50,22 @@ fun SearchBar(
|
|||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
var isInitialComposition by rememberSaveable { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
isInitialComposition = false
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = TangemTheme.dimens.size48)
|
||||
.onFocusChanged { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
state.onActiveChange(true)
|
||||
} else {
|
||||
state.onActiveChange(false)
|
||||
if (!isInitialComposition) {
|
||||
if (focusState.isFocused) {
|
||||
state.onActiveChange(true)
|
||||
} else {
|
||||
state.onActiveChange(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
.focusRequester(focusRequester)
|
||||
|
|
@ -163,6 +168,7 @@ private fun ClearButton(
|
|||
focusManager.clearFocus()
|
||||
keyboardController?.hide()
|
||||
state.onActiveChange(false)
|
||||
state.onClearClick()
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ data class SearchBarUM(
|
|||
val onQueryChange: (String) -> Unit,
|
||||
val isActive: Boolean,
|
||||
val onActiveChange: (Boolean) -> Unit,
|
||||
val onClearClick: () -> Unit = {},
|
||||
)
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.core.ui.components.haze
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.res.LocalHazeState
|
||||
import com.tangem.core.ui.res.LocalPowerSavingState
|
||||
import com.tangem.core.ui.res.LocalRootBackgroundColor
|
||||
import dev.chrisbanes.haze.*
|
||||
import dev.chrisbanes.haze.materials.CupertinoMaterials
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
|
||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
internal fun ProvideHaze(content: @Composable () -> Unit) {
|
||||
val hazeState = rememberHazeState()
|
||||
CompositionLocalProvider(
|
||||
LocalHazeState provides hazeState,
|
||||
LocalHazeStyle provides CupertinoMaterials.ultraThin(),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a haze effect to the [Modifier] with consideration of global haze settings and power saving mode.
|
||||
*
|
||||
* @param configure A lambda to configure the [HazeEffectScope].
|
||||
* @return A [Modifier] with the configured haze effect applied.
|
||||
*/
|
||||
@Composable
|
||||
fun Modifier.hazeEffectTangem(
|
||||
style: HazeStyle = HazeStyle.Unspecified,
|
||||
configure: HazeEffectScope.() -> Unit = {},
|
||||
): Modifier {
|
||||
val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
|
||||
val hazeState = LocalHazeState.current
|
||||
val isGlobalBlurEnabled = hazeState.blurEnabled && !powerSavingEnabled.value
|
||||
val rootBackground by LocalRootBackgroundColor.current
|
||||
|
||||
return hazeEffect(hazeState, style) {
|
||||
fallbackTint = HazeTint(rootBackground)
|
||||
if (isGlobalBlurEnabled) {
|
||||
configure()
|
||||
}
|
||||
blurEnabled = isGlobalBlurEnabled
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a haze source to the [Modifier] using the current global haze state.
|
||||
*/
|
||||
@Composable
|
||||
fun Modifier.hazeSourceTangem(zIndex: Float = 0f, key: Any? = null) =
|
||||
this.hazeSource(LocalHazeState.current, zIndex, key)
|
||||
|
|
@ -7,4 +7,5 @@ enum class IconTint {
|
|||
Accent,
|
||||
Warning,
|
||||
Inactive,
|
||||
Informative,
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ 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.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -31,6 +32,7 @@ import com.tangem.core.ui.extensions.resolveReference
|
|||
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.SendSelectNetworkFeeBottomSheetTestTags
|
||||
|
||||
/**
|
||||
* [InputRowEnter](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&t=IQ5lBJEkFGU4WSvi-4)
|
||||
|
|
@ -92,7 +94,7 @@ fun InputRowEnter(
|
|||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clip(CircleShape),
|
||||
text = description.resolveReference(),
|
||||
text = description,
|
||||
content = { contentModifier ->
|
||||
Icon(
|
||||
modifier = contentModifier.size(16.dp),
|
||||
|
|
@ -113,7 +115,8 @@ fun InputRowEnter(
|
|||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8),
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_TEXT_FIELD),
|
||||
)
|
||||
}
|
||||
iconRes?.let { iconRes ->
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ 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.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -21,6 +22,7 @@ import com.tangem.core.ui.components.tooltip.TangemTooltip
|
|||
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.test.SendSelectNetworkFeeBottomSheetTestTags
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
|
||||
/**
|
||||
|
|
@ -112,6 +114,7 @@ fun InputRowEnterInfoAmount(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun InputRowEnterInfoAmountV2(
|
||||
title: TextReference,
|
||||
|
|
@ -139,20 +142,23 @@ fun InputRowEnterInfoAmountV2(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
.padding(16.dp)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = titleColor,
|
||||
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TITLE),
|
||||
)
|
||||
if (description != null) {
|
||||
TangemTooltip(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clip(CircleShape),
|
||||
text = description.resolveReference(),
|
||||
.clip(CircleShape)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TOOLTIP_ICON),
|
||||
text = description,
|
||||
content = { contentModifier ->
|
||||
Icon(
|
||||
modifier = contentModifier.size(16.dp),
|
||||
|
|
@ -183,7 +189,8 @@ fun InputRowEnterInfoAmountV2(
|
|||
backgroundColor = Color.Transparent,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.weight(1f),
|
||||
.weight(1f)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD),
|
||||
)
|
||||
info?.let { info ->
|
||||
Text(
|
||||
|
|
@ -192,7 +199,8 @@ fun InputRowEnterInfoAmountV2(
|
|||
color = infoColor,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8)
|
||||
.align(Alignment.Bottom),
|
||||
.align(Alignment.Bottom)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_FIAT_AMOUNT),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
|||
val backgroundColor by animateColorAsState(
|
||||
targetValue = when (state.style) {
|
||||
LabelStyle.ACCENT -> TangemTheme.colors.text.accent.copy(alpha = 0.1f)
|
||||
LabelStyle.REGULAR -> TangemTheme.colors.control.unchecked
|
||||
LabelStyle.REGULAR -> TangemTheme.colors.control.default
|
||||
LabelStyle.WARNING -> TangemTheme.colors.text.warning.copy(alpha = 0.1f)
|
||||
},
|
||||
)
|
||||
|
|
@ -168,7 +168,9 @@ private fun LabelPreview() {
|
|||
TangemThemePreview {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ data class LabelUM(
|
|||
@DrawableRes val icon: Int? = null,
|
||||
val onIconClick: (() -> Unit)? = null,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
val maxLines: Int = Int.MAX_VALUE,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.core.ui.components.pager
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
|
|
@ -8,18 +9,24 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.getValue
|
||||
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.Shape
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
// six - cause the central indicator has width multiplied twice
|
||||
private const val TOTAL_MAX_INDICATORS = 6
|
||||
private const val SPACER_COUNT_BETWEEN_INDICATORS = 4
|
||||
|
||||
/**
|
||||
* Horizontal pager indicator
|
||||
|
|
@ -29,82 +36,163 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
*/
|
||||
@Composable
|
||||
fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier, indicatorCount: Int = 5) {
|
||||
if (pagerState.pageCount == 0) return
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val indicatorColor = TangemTheme.colors.control.key
|
||||
val overlayColor = TangemTheme.colors.overlay.secondary
|
||||
val indicatorSize = 8.dp
|
||||
|
||||
val inactiveIndicatorColor = remember(indicatorColor) {
|
||||
indicatorColor.copy(alpha = 0.5f)
|
||||
}
|
||||
|
||||
val baseIndicatorSize = 8.dp
|
||||
val spacing = 4.dp
|
||||
|
||||
val totalWidth: Dp = indicatorSize * indicatorCount + spacing * (indicatorCount - 1)
|
||||
val widthInPx = LocalDensity.current.run { indicatorSize.toPx() }
|
||||
|
||||
val currentItem by remember {
|
||||
val indicatorState by remember(pagerState, indicatorCount) {
|
||||
derivedStateOf {
|
||||
pagerState.currentPage
|
||||
val count = pagerState.pageCount
|
||||
val current = pagerState.currentPage
|
||||
|
||||
val winSize = min(indicatorCount, count)
|
||||
val centerPosition = winSize / 2
|
||||
|
||||
val start = when {
|
||||
count <= winSize -> 0
|
||||
current <= centerPosition -> 0
|
||||
current >= count - centerPosition - 1 -> count - winSize
|
||||
else -> current - centerPosition
|
||||
}
|
||||
Triple(count, winSize, start)
|
||||
}
|
||||
}
|
||||
|
||||
val itemCount = pagerState.pageCount
|
||||
val (itemCount, windowSize, windowStart) = indicatorState
|
||||
val currentItem by remember { derivedStateOf { pagerState.currentPage } }
|
||||
|
||||
LaunchedEffect(key1 = currentItem) {
|
||||
val viewportSize = listState.layoutInfo.viewportSize
|
||||
listState.animateScrollToItem(
|
||||
currentItem,
|
||||
(widthInPx / 2 - viewportSize.width / 2).toInt(),
|
||||
)
|
||||
LaunchedEffect(currentItem, windowStart) {
|
||||
if (itemCount > windowSize) {
|
||||
listState.animateScrollToItem(windowStart.coerceIn(0, itemCount - 1))
|
||||
}
|
||||
}
|
||||
|
||||
val maxContainerWidth = remember(baseIndicatorSize, spacing) {
|
||||
baseIndicatorSize * TOTAL_MAX_INDICATORS + spacing * SPACER_COUNT_BETWEEN_INDICATORS
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(32.dp)
|
||||
.width(maxContainerWidth + 32.dp)
|
||||
.background(
|
||||
color = overlayColor,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.clip(CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.width(totalWidth),
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
state = listState,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing),
|
||||
userScrollEnabled = false,
|
||||
) {
|
||||
indicatorItems(
|
||||
itemCount = itemCount,
|
||||
currentItem = currentItem,
|
||||
indicatorShape = CircleShape,
|
||||
activeColor = indicatorColor,
|
||||
inActiveColor = indicatorColor.copy(alpha = 0.5f),
|
||||
indicatorSize = indicatorSize,
|
||||
inActiveColor = inactiveIndicatorColor,
|
||||
baseSize = baseIndicatorSize,
|
||||
windowSize = windowSize,
|
||||
windowStart = windowStart,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber", "CyclomaticComplexMethod")
|
||||
private fun calculateIndicatorHeight(position: Int, currentPosition: Int, baseSize: Dp, windowSize: Int): Dp {
|
||||
val distance = abs(position - currentPosition)
|
||||
val mediumSize = 6.dp
|
||||
val smallSize = 4.dp
|
||||
|
||||
if (windowSize < 5) {
|
||||
return when {
|
||||
distance <= 1 -> baseSize
|
||||
distance == 2 -> mediumSize
|
||||
else -> smallSize
|
||||
}
|
||||
}
|
||||
|
||||
val isEdgeFocus = currentPosition == 0 || currentPosition == windowSize - 1
|
||||
val isNearEdgeFocus = currentPosition == 1 || currentPosition == windowSize - 2
|
||||
return when {
|
||||
isEdgeFocus -> when {
|
||||
distance <= 2 -> baseSize
|
||||
distance == 3 -> mediumSize
|
||||
else -> smallSize
|
||||
}
|
||||
isNearEdgeFocus -> when {
|
||||
distance <= 1 -> baseSize
|
||||
distance == 2 -> mediumSize
|
||||
else -> smallSize
|
||||
}
|
||||
else -> when {
|
||||
distance <= 1 -> baseSize
|
||||
else -> mediumSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun LazyListScope.indicatorItems(
|
||||
itemCount: Int,
|
||||
currentItem: Int,
|
||||
indicatorShape: Shape,
|
||||
activeColor: Color,
|
||||
inActiveColor: Color,
|
||||
indicatorSize: Dp,
|
||||
baseSize: Dp,
|
||||
windowSize: Int,
|
||||
windowStart: Int,
|
||||
) {
|
||||
items(itemCount) { index ->
|
||||
val safeWindowSize = min(windowSize, itemCount)
|
||||
if (safeWindowSize <= 0) return
|
||||
|
||||
val isSelected = index == currentItem
|
||||
val windowEnd = windowStart + safeWindowSize
|
||||
val currentPosInWindow = (currentItem - windowStart).coerceIn(0, safeWindowSize - 1)
|
||||
|
||||
items(itemCount) { pageIndex ->
|
||||
val isInWindow = pageIndex in windowStart until windowEnd
|
||||
val positionInWindow = (pageIndex - windowStart).coerceIn(0, safeWindowSize - 1)
|
||||
|
||||
val isSelected = pageIndex == currentItem
|
||||
|
||||
val refinedHeight = if (isInWindow) {
|
||||
calculateIndicatorHeight(
|
||||
position = positionInWindow,
|
||||
currentPosition = currentPosInWindow,
|
||||
baseSize = baseSize,
|
||||
windowSize = safeWindowSize,
|
||||
)
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
val targetWidth = if (isSelected) refinedHeight * 2 else refinedHeight
|
||||
val targetShape = if (isSelected) RoundedCornerShape(16.dp) else CircleShape
|
||||
val animatedWidth by animateDpAsState(targetValue = targetWidth, label = "width")
|
||||
val animatedHeight by animateDpAsState(targetValue = refinedHeight, label = "height")
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(indicatorShape)
|
||||
.size(indicatorSize)
|
||||
.padding(vertical = (baseSize - animatedHeight) / 2)
|
||||
.clip(targetShape)
|
||||
.width(animatedWidth)
|
||||
.height(animatedHeight)
|
||||
.background(
|
||||
if (isSelected) activeColor else inActiveColor,
|
||||
indicatorShape,
|
||||
targetShape,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -112,19 +200,31 @@ private fun LazyListScope.indicatorItems(
|
|||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicatorPreviewFirstPage() {
|
||||
private fun PagerIndicatorPreview() {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(),
|
||||
contentAlignment = Alignment.Center,
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = 0,
|
||||
initialPage = 2,
|
||||
pageCount = { 10 },
|
||||
)
|
||||
PagerIndicator(pagerState = pagerState)
|
||||
|
||||
val pagerState1 = rememberPagerState(
|
||||
initialPage = 0,
|
||||
pageCount = { 3 },
|
||||
)
|
||||
PagerIndicator(pagerState = pagerState1)
|
||||
|
||||
val pagerState2 = rememberPagerState(
|
||||
initialPage = 0,
|
||||
pageCount = { 1 },
|
||||
)
|
||||
PagerIndicator(pagerState = pagerState2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.core.ui.components.powersaving
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.PowerManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class PowerSavingState(
|
||||
appContext: Context,
|
||||
lifecycle: androidx.lifecycle.Lifecycle,
|
||||
) {
|
||||
|
||||
private val powerManager =
|
||||
appContext.getSystemService(Context.POWER_SERVICE) as? PowerManager ?: error("PowerManager not available")
|
||||
|
||||
val isPowerSavingModeEnabled: StateFlow<Boolean>
|
||||
field = MutableStateFlow(powerManager.isPowerSaveMode)
|
||||
|
||||
init {
|
||||
val intentFilter = IntentFilter().apply {
|
||||
addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)
|
||||
}
|
||||
|
||||
val receiver = PowerSavingModeReceiver()
|
||||
|
||||
ContextCompat.registerReceiver(
|
||||
appContext,
|
||||
receiver,
|
||||
intentFilter,
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||
)
|
||||
|
||||
lifecycle.addObserver(object : DefaultLifecycleObserver {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
appContext.unregisterReceiver(receiver)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
inner class PowerSavingModeReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) {
|
||||
isPowerSavingModeEnabled.value = powerManager.isPowerSaveMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun rememberPowerSavingState(): PowerSavingState {
|
||||
val appContext = LocalContext.current.applicationContext
|
||||
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||
return remember(appContext, lifecycle) {
|
||||
PowerSavingState(appContext = appContext, lifecycle = lifecycle)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
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.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.SpacerH28
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
/**
|
||||
* 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(title: String, description: String, modifier: Modifier = Modifier, isClickable: Boolean = true) {
|
||||
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),
|
||||
) {
|
||||
AnimatedContent(targetState = title, label = "") { title ->
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
AnimatedContent(targetState = description, label = "") { description ->
|
||||
Text(
|
||||
text = description,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SimpleActionRowPreview() {
|
||||
Column {
|
||||
TangemThemePreview(isDark = false) {
|
||||
SimpleActionRow(
|
||||
title = "Title",
|
||||
description = "Description",
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH28()
|
||||
|
||||
TangemThemePreview(isDark = false) {
|
||||
SimpleActionRow(
|
||||
title = "Title",
|
||||
description = "Description",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ import com.tangem.core.ui.extensions.resolveReference
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
|
||||
import com.tangem.core.ui.test.SwapSelectNetworkFeeBottomSheetTestTags
|
||||
import com.tangem.utils.StringsSigns
|
||||
|
||||
@Composable
|
||||
|
|
@ -70,7 +70,7 @@ fun SelectorRowItem(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(paddingValues)
|
||||
.testTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM),
|
||||
.testTag(SwapSelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
|
|
|||
|
|
@ -239,19 +239,19 @@ private fun StandardBottomSheet(
|
|||
}
|
||||
val newTarget =
|
||||
when (val oldTarget = state.anchoredDraggableState.targetValue) {
|
||||
Hidden -> if (newAnchors.hasAnchorFor(Hidden)) Hidden else oldTarget
|
||||
Hidden -> if (newAnchors.hasPositionFor(Hidden)) Hidden else oldTarget
|
||||
PartiallyExpanded ->
|
||||
when {
|
||||
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
|
||||
newAnchors.hasAnchorFor(Expanded) -> Expanded
|
||||
newAnchors.hasAnchorFor(Hidden) -> Hidden
|
||||
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
|
||||
newAnchors.hasPositionFor(Expanded) -> Expanded
|
||||
newAnchors.hasPositionFor(Hidden) -> Hidden
|
||||
else -> oldTarget
|
||||
}
|
||||
Expanded ->
|
||||
when {
|
||||
newAnchors.hasAnchorFor(Expanded) -> Expanded
|
||||
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
|
||||
newAnchors.hasAnchorFor(Hidden) -> Hidden
|
||||
newAnchors.hasPositionFor(Expanded) -> Expanded
|
||||
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
|
||||
newAnchors.hasPositionFor(Hidden) -> Hidden
|
||||
else -> oldTarget
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,11 +102,11 @@ class TangemSheetState(
|
|||
|
||||
/** Whether the sheet has an expanded state defined. */
|
||||
val hasExpandedState: Boolean
|
||||
get() = anchoredDraggableState.anchors.hasAnchorFor(Expanded)
|
||||
get() = anchoredDraggableState.anchors.hasPositionFor(Expanded)
|
||||
|
||||
/** Whether the modal bottom sheet has a partially expanded state defined. */
|
||||
val hasPartiallyExpandedState: Boolean
|
||||
get() = anchoredDraggableState.anchors.hasAnchorFor(PartiallyExpanded)
|
||||
get() = anchoredDraggableState.anchors.hasPositionFor(PartiallyExpanded)
|
||||
|
||||
/**
|
||||
* Fully expand the bottom sheet with animation and suspend until it is fully expanded or
|
||||
|
|
@ -274,8 +274,8 @@ internal fun consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
|||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
val toFling = available.toFloat()
|
||||
val currentOffset = sheetState.requireOffset()
|
||||
val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor()
|
||||
return if (toFling < 0 && currentOffset > minAnchor) {
|
||||
val minPosition = sheetState.anchoredDraggableState.anchors.minPosition()
|
||||
return if (toFling < 0 && currentOffset > minPosition) {
|
||||
onFling(toFling)
|
||||
// since we go to the anchor with tween settling, consume all for the best UX
|
||||
available
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
package com.tangem.core.ui.components.states
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class SelectableItemsState<T>(
|
||||
val selectedItem: Item<T>,
|
||||
val items: ImmutableList<Item<T>>,
|
||||
)
|
||||
|
||||
data class Item<T>(
|
||||
val id: Int,
|
||||
val startText: TextReference,
|
||||
val endText: TextReference,
|
||||
val isSelected: Boolean,
|
||||
val data: T,
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.core.ui.components.token.internal
|
|||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
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
|
||||
|
|
@ -43,16 +44,33 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool
|
|||
isFlickering = state.isFlickering,
|
||||
)
|
||||
}
|
||||
is TokenFiatAmountState.Icon -> {
|
||||
IconAmount(modifier = modifier, state = state)
|
||||
}
|
||||
is TokenFiatAmountState.Loading -> {
|
||||
RectangleShimmer(modifier = modifier.placeholderSize(), radius = 4.dp)
|
||||
}
|
||||
is TokenFiatAmountState.Locked -> {
|
||||
LockedRectangle(modifier = modifier.placeholderSize())
|
||||
}
|
||||
is TokenFiatAmountState.Empty -> {
|
||||
// Empty box for proper measurements
|
||||
Box(modifier)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IconAmount(state: TokenFiatAmountState.Icon, modifier: Modifier = Modifier) {
|
||||
Icon(
|
||||
modifier = modifier,
|
||||
painter = rememberVectorPainter(image = ImageVector.vectorResource(state.iconRes)),
|
||||
tint = state.tint.color(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentFiatAmount(
|
||||
text: String,
|
||||
|
|
@ -76,11 +94,7 @@ private fun ContentFiatAmount(
|
|||
Icon(
|
||||
modifier = Modifier.size(12.dp),
|
||||
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
|
||||
tint = when (icon.tint) {
|
||||
IconTint.Accent -> TangemTheme.colors.icon.accent
|
||||
IconTint.Warning -> TangemTheme.colors.icon.attention
|
||||
IconTint.Inactive -> TangemTheme.colors.icon.inactive
|
||||
},
|
||||
tint = icon.tint.color(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -91,6 +105,14 @@ private fun ContentFiatAmount(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IconTint.color() = when (this) {
|
||||
IconTint.Accent -> TangemTheme.colors.icon.accent
|
||||
IconTint.Warning -> TangemTheme.colors.icon.attention
|
||||
IconTint.Inactive -> TangemTheme.colors.icon.inactive
|
||||
IconTint.Informative -> TangemTheme.colors.icon.informative
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FiatAmountText(
|
||||
text: String,
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ private fun YieldSupplyApyLabel(apy: TextReference?, isActive: Boolean, modifier
|
|||
color = if (isActive) {
|
||||
TangemTheme.colors.text.accent.copy(alpha = 0.1f)
|
||||
} else {
|
||||
TangemTheme.colors.control.unchecked
|
||||
TangemTheme.colors.control.default
|
||||
},
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ internal fun YieldSupplyPromoBanner(state: PromoBannerState.Content, modifier: M
|
|||
LaunchedEffect(state) {
|
||||
state.onPromoShown()
|
||||
}
|
||||
val bgColor = TangemTheme.colors.control.unchecked
|
||||
val bgColor = TangemTheme.colors.control.default
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -235,9 +235,16 @@ sealed class TokenItemState {
|
|||
val isFlickering: Boolean = false,
|
||||
) : FiatAmountState()
|
||||
|
||||
data class Icon(
|
||||
val iconRes: Int,
|
||||
val tint: IconTint = IconTint.Inactive,
|
||||
) : FiatAmountState()
|
||||
|
||||
data object Loading : FiatAmountState()
|
||||
|
||||
data object Locked : FiatAmountState()
|
||||
|
||||
data object Empty : FiatAmountState()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -1,29 +1,39 @@
|
|||
package com.tangem.core.ui.components.tooltip
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* A Tangem-themed tooltip component that displays a tooltip with the provided text when the content is clicked.
|
||||
*
|
||||
* @param text The text to be displayed inside the tooltip.
|
||||
* @param modifier The modifier to be applied to the tooltip component.
|
||||
* @param enabled If false, the tooltip will not be shown when the content is clicked.
|
||||
* @param content The content that triggers the tooltip when clicked.
|
||||
*/
|
||||
@Composable
|
||||
fun TangemTooltip(
|
||||
text: String,
|
||||
text: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
content: @Composable (Modifier) -> Unit,
|
||||
|
|
@ -33,30 +43,10 @@ fun TangemTooltip(
|
|||
enabled = enabled,
|
||||
tooltipContent = {
|
||||
Text(
|
||||
modifier = Modifier.background(TangemTheme.colors.icon.secondary),
|
||||
text = text,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
)
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TangemTooltip(
|
||||
text: AnnotatedString,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
content: @Composable (Modifier) -> Unit,
|
||||
) {
|
||||
InternalTangemTooltip(
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
tooltipContent = {
|
||||
Text(
|
||||
modifier = Modifier.background(TangemTheme.colors.icon.secondary),
|
||||
text = text,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.icon.secondary)
|
||||
.padding(horizontal = 6.dp, vertical = 8.dp),
|
||||
text = text.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
)
|
||||
|
|
@ -75,16 +65,22 @@ private fun InternalTangemTooltip(
|
|||
) {
|
||||
val tooltipState = rememberTooltipState(isPersistent = true)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val windowSize = LocalWindowSize.current.width
|
||||
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(spacingBetweenTooltipAndAnchor = 8.dp),
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||
positioning = TooltipAnchorPosition.Above,
|
||||
spacingBetweenTooltipAndAnchor = 8.dp,
|
||||
),
|
||||
state = tooltipState,
|
||||
modifier = modifier,
|
||||
tooltip = {
|
||||
PlainTooltip(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
caretSize = DpSize(width = 14.dp, height = 8.dp),
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
caretShape = TooltipDefaults.caretShape(),
|
||||
maxWidth = windowSize - 24.dp,
|
||||
contentColor = TangemTheme.colors.text.primary2,
|
||||
containerColor = TangemTheme.colors.icon.secondary,
|
||||
content = { tooltipContent() },
|
||||
|
|
@ -103,24 +99,23 @@ private fun InternalTangemTooltip(
|
|||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 720)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun TangemTooltip_Preview() {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(500.dp)
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
contentAlignment = Alignment.Center,
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
TangemTooltip(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.size(64.dp),
|
||||
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis.",
|
||||
modifier = Modifier,
|
||||
text = stringReference("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis."),
|
||||
content = { contentModifier ->
|
||||
Icon(
|
||||
modifier = contentModifier.size(64.dp),
|
||||
modifier = contentModifier,
|
||||
painter = painterResource(R.drawable.ic_token_info_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
* @param txHistoryItems transactions
|
||||
* @param modifier modifier
|
||||
*/
|
||||
@Suppress("CanBeNonNullable")
|
||||
fun LazyListScope.txHistoryItems(
|
||||
state: TxHistoryState,
|
||||
txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>?,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.core.ui.decompose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
|
||||
/**
|
||||
* An interface describing the UI part of a component for a modular BottomSheet.
|
||||
*
|
||||
* Designed for use in Decompose components. It separates the UI into a title and content,
|
||||
* providing access to the [BottomSheetState] to react to changes in the sheet's state (collapsed/expanded).
|
||||
*/
|
||||
@Stable
|
||||
interface ComposableModularBottomSheetContentComponent {
|
||||
|
||||
/**
|
||||
* Renders the title of the bottom sheet.
|
||||
* @param bottomSheetState The current state of the bottom sheet. This can be used, for example,
|
||||
* to change navigation buttons (e.g., hiding the "Back" button when collapsed).
|
||||
*/
|
||||
@Composable
|
||||
fun Title(bottomSheetState: State<BottomSheetState>)
|
||||
|
||||
/**
|
||||
* Renders the main content of the bottom sheet.
|
||||
* @param bottomSheetState The current state of the bottom sheet. Useful for tracking visibility
|
||||
* (e.g., for analytics or lifecycle effects when the sheet is [BottomSheetState.EXPANDED]).
|
||||
*/
|
||||
@Composable
|
||||
fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier)
|
||||
}
|
||||
344
core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt
Normal file
344
core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
package com.tangem.core.ui.ds.badge
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
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.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.ds.badge.TangemBadgeSize.*
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
/**
|
||||
* Tangem badge component to display a small piece of information with optional icon.
|
||||
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8441-83535&m=dev)
|
||||
*
|
||||
* @param badgeUM [TangemBadgeUM] containing all the badge parameters.
|
||||
* @param modifier Modifier to be applied to the badge.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
|
||||
TangemBadge(
|
||||
text = badgeUM.text,
|
||||
iconRes = badgeUM.iconRes,
|
||||
size = badgeUM.size,
|
||||
shape = badgeUM.shape,
|
||||
color = badgeUM.color,
|
||||
type = badgeUM.type,
|
||||
iconPosition = badgeUM.iconPosition,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tangem badge component to display a small piece of information with optional icon.
|
||||
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8441-83535&m=dev)
|
||||
*
|
||||
* @param text TextReference for the badge label.
|
||||
* @param modifier Modifier to be applied to the badge.
|
||||
* @param iconRes Drawable resource ID for the icon to be displayed in the badge.
|
||||
* @param size [TangemBadgeSize] defining the size of the badge.
|
||||
* @param shape [TangemBadgeShape] defining the shape of the badge.
|
||||
* @param color [TangemBadgeColor] defining the color scheme of the badge.
|
||||
* @param type [TangemBadgeType] defining the style of the badge.
|
||||
* @param iconPosition [TangemBadgeIconPosition] defining icon position of the badge.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun TangemBadge(
|
||||
text: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
@DrawableRes iconRes: Int? = null,
|
||||
size: TangemBadgeSize = X9,
|
||||
shape: TangemBadgeShape = TangemBadgeShape.Default,
|
||||
color: TangemBadgeColor = TangemBadgeColor.Gray,
|
||||
type: TangemBadgeType = TangemBadgeType.Solid,
|
||||
iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start,
|
||||
) {
|
||||
val iconColor = getIconColor(type = type, color = color)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(size.toContentPadding()),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.heightIn(min = size.toHeightDp())
|
||||
.clip(shape.toShape(size))
|
||||
.getBackgroundColor(type = type, color = color, shape = shape.toShape(size))
|
||||
.padding(size.toPaddingDp(position = iconPosition)),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
label = "Start Icon Visibility",
|
||||
) {
|
||||
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
|
||||
Icon(
|
||||
painter = painterResource(id = wrappedIconRes),
|
||||
contentDescription = null,
|
||||
tint = iconColor,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = text.resolveReference(),
|
||||
style = size.toTextStyle(),
|
||||
maxLines = 1,
|
||||
color = getTextColor(type = type, color = color),
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
label = "End Icon Visibility",
|
||||
) {
|
||||
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
|
||||
Icon(
|
||||
painter = painterResource(id = wrappedIconRes),
|
||||
contentDescription = null,
|
||||
tint = iconColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tangem badge shape options.
|
||||
*/
|
||||
enum class TangemBadgeShape {
|
||||
Default,
|
||||
Rounded,
|
||||
;
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
internal fun toShape(size: TangemBadgeSize) = RoundedCornerShape(
|
||||
when (this) {
|
||||
Rounded -> when (size) {
|
||||
X4,
|
||||
X6,
|
||||
-> TangemTheme.dimens2.x4
|
||||
X9 -> TangemTheme.dimens2.x25
|
||||
}
|
||||
Default -> when (size) {
|
||||
X4 -> TangemTheme.dimens2.x1
|
||||
X6,
|
||||
X9,
|
||||
-> 6.dp
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tangem badge size options.
|
||||
*/
|
||||
enum class TangemBadgeSize {
|
||||
X4,
|
||||
X6,
|
||||
X9,
|
||||
;
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
internal fun toHeightDp() = when (this) {
|
||||
X4 -> TangemTheme.dimens2.x4
|
||||
X6 -> TangemTheme.dimens2.x6
|
||||
X9 -> TangemTheme.dimens2.x9
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
internal fun toPaddingDp(position: TangemBadgeIconPosition) = when (this) {
|
||||
X4 -> when (position) {
|
||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 4.dp, end = 6.dp)
|
||||
TangemBadgeIconPosition.End -> PaddingValues(start = 6.dp, end = 4.dp)
|
||||
}
|
||||
X6 -> when (position) {
|
||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 8.dp, end = 12.dp)
|
||||
TangemBadgeIconPosition.End -> PaddingValues(start = 12.dp, end = 8.dp)
|
||||
}
|
||||
X9 -> when (position) {
|
||||
TangemBadgeIconPosition.Start -> PaddingValues(start = 12.dp, end = 16.dp)
|
||||
TangemBadgeIconPosition.End -> PaddingValues(start = 16.dp, end = 12.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
internal fun toContentSize() = when (this) {
|
||||
X4 -> TangemTheme.dimens2.x3
|
||||
X6,
|
||||
X9,
|
||||
-> TangemTheme.dimens2.x4
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
internal fun toContentPadding() = when (this) {
|
||||
X4 -> TangemTheme.dimens2.x0_5
|
||||
X6,
|
||||
X9,
|
||||
-> TangemTheme.dimens2.x1
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
internal fun toTextStyle() = when (this) {
|
||||
X4 -> TangemTheme.typography2.captionSemibold11
|
||||
X6 -> TangemTheme.typography2.captionSemibold12
|
||||
X9 -> TangemTheme.typography2.bodySemibold16
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Position of the icon in the Tangem badge.
|
||||
*/
|
||||
enum class TangemBadgeIconPosition {
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
/**
|
||||
* Tangem badge type options.
|
||||
*/
|
||||
enum class TangemBadgeType {
|
||||
Solid,
|
||||
Tinted,
|
||||
Outline,
|
||||
}
|
||||
|
||||
/**
|
||||
* Tangem badge color options.
|
||||
*/
|
||||
enum class TangemBadgeColor {
|
||||
Blue,
|
||||
Red,
|
||||
Gray,
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
private fun getIconColor(type: TangemBadgeType, color: TangemBadgeColor) = when (color) {
|
||||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.iconGray
|
||||
TangemBadgeColor.Blue -> when (type) {
|
||||
TangemBadgeType.Outline,
|
||||
TangemBadgeType.Tinted,
|
||||
-> TangemTheme.colors2.markers.iconBlue
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
|
||||
}
|
||||
TangemBadgeColor.Red -> when (type) {
|
||||
TangemBadgeType.Outline,
|
||||
TangemBadgeType.Tinted,
|
||||
-> TangemTheme.colors2.markers.iconRed
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
|
||||
}
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
private fun getTextColor(type: TangemBadgeType, color: TangemBadgeColor) = when (color) {
|
||||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.textGray
|
||||
TangemBadgeColor.Blue -> when (type) {
|
||||
TangemBadgeType.Outline,
|
||||
TangemBadgeType.Tinted,
|
||||
-> TangemTheme.colors2.markers.textBlue
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
|
||||
}
|
||||
TangemBadgeColor.Red -> when (type) {
|
||||
TangemBadgeType.Outline,
|
||||
TangemBadgeType.Tinted,
|
||||
-> TangemTheme.colors2.markers.textRed
|
||||
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
|
||||
}
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadgeColor, shape: Shape) = when (type) {
|
||||
TangemBadgeType.Solid -> background(
|
||||
when (color) {
|
||||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundSolidGray
|
||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundSolidBlue
|
||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundSolidRed
|
||||
},
|
||||
)
|
||||
TangemBadgeType.Tinted -> background(
|
||||
when (color) {
|
||||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundTintedGray
|
||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundTintedBlue
|
||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundTintedRed
|
||||
},
|
||||
)
|
||||
TangemBadgeType.Outline -> {
|
||||
border(
|
||||
color = when (color) {
|
||||
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.borderGray
|
||||
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.borderTintedBlue
|
||||
TangemBadgeColor.Red -> TangemTheme.colors2.markers.borderTintedRed
|
||||
},
|
||||
shape = shape,
|
||||
width = 1.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun TangemBadge_Preview(@PreviewParameter(TangemBadgePreviewProvider::class) params: TangemBadgeColor) {
|
||||
TangemThemePreviewRedesign {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors2.surface.level1)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
repeat(2) { yIndex ->
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
repeat(TangemBadgeType.entries.size) { index ->
|
||||
TangemBadge(
|
||||
text = stringReference("Title"),
|
||||
iconRes = R.drawable.ic_information_24,
|
||||
type = TangemBadgeType.entries[index],
|
||||
color = params,
|
||||
shape = TangemBadgeShape.entries[yIndex % 2],
|
||||
iconPosition = TangemBadgeIconPosition.entries[yIndex % 2],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TangemBadgePreviewProvider : PreviewParameterProvider<TangemBadgeColor> {
|
||||
override val values: Sequence<TangemBadgeColor>
|
||||
get() = sequenceOf(
|
||||
TangemBadgeColor.Gray,
|
||||
TangemBadgeColor.Blue,
|
||||
TangemBadgeColor.Red,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.core.ui.ds.badge
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.ds.badge.TangemBadgeSize.X9
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
* UI model for [TangemBadge] component
|
||||
*
|
||||
* @param text TextReference for the badge label.
|
||||
* @param modifier Modifier to be applied to the badge.
|
||||
* @param iconRes Drawable resource ID for the icon to be displayed in the badge.
|
||||
* @param size [TangemBadgeSize] defining the size of the badge.
|
||||
* @param shape [TangemBadgeShape] defining the shape of the badge.
|
||||
* @param color [TangemBadgeColor] defining the color scheme of the badge.
|
||||
* @param type [TangemBadgeType] defining the style of the badge.
|
||||
* @param iconPosition [TangemBadgeIconPosition] defining icon position of the badge.
|
||||
*/
|
||||
class TangemBadgeUM(
|
||||
val text: TextReference,
|
||||
@DrawableRes val iconRes: Int? = null,
|
||||
val size: TangemBadgeSize = X9,
|
||||
val shape: TangemBadgeShape = TangemBadgeShape.Default,
|
||||
val color: TangemBadgeColor = TangemBadgeColor.Gray,
|
||||
val type: TangemBadgeType = TangemBadgeType.Solid,
|
||||
val iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start,
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue