Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-24 10:50:03 +01:00
commit bc84d8f5f8
579 changed files with 13604 additions and 3422 deletions

View file

@ -25,6 +25,7 @@ dependencies {
implementation(projects.domain.wallets.models)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.staking) // TODO staking create staking models module
/** Tangem libraries */
implementation(deps.tangem.blockchain)

View file

@ -2,12 +2,40 @@ package com.tangem.datasource.api.common.adapter
import com.squareup.moshi.*
import com.squareup.moshi.adapters.EnumJsonAdapter
import com.tangem.datasource.api.stakekit.models.response.model.*
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO
/**
* Object to create a adapter for enum types with support for unknown enum values.
*/
object UnknownEnumMoshiAdapter {
fun <T : Enum<T>> create(enumType: Class<T>, defaultValue: T): JsonAdapter<T> =
EnumJsonAdapter.create(enumType).withUnknownFallback(defaultValue)
@Suppress("UNCHECKED_CAST")
fun <T : Enum<T>> create(enumType: Class<out Enum<*>>, defaultValue: Enum<*>): JsonAdapter<out Enum<*>> {
return EnumJsonAdapter.create(enumType as Class<T>).withUnknownFallback(defaultValue as T)
}
}
fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder {
val map = mapOf(
NetworkTypeDTO::class.java to NetworkTypeDTO.UNKNOWN,
StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN,
YieldDTO.RewardTypeDTO::class.java to YieldDTO.RewardTypeDTO.UNKNOWN,
YieldBalanceWrapperDTO.BalanceDTO.BalanceType::class.java to
YieldBalanceWrapperDTO.BalanceDTO.BalanceType.UNKNOWN,
StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN,
StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN,
StakingActionStatusDTO::class.java to StakingActionStatusDTO.UNKNOWN,
)
return apply {
map.forEach { entry ->
val enumClass = entry.key
val unknownValue = entry.value
add(enumClass, UnknownEnumMoshiAdapter.create(enumClass, unknownValue))
}
}
}

View file

@ -1,48 +1,60 @@
package com.tangem.datasource.api.stakekit
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
import com.tangem.datasource.api.stakekit.models.request.RevenueOption
import com.tangem.datasource.api.stakekit.models.request.YieldType
import com.tangem.datasource.api.stakekit.models.request.*
import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse
import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYield
import com.tangem.datasource.api.stakekit.models.response.model.Yield
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapper
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import retrofit2.http.*
@Suppress("LongParameterList")
interface StakeKitApi {
@GET("yields/enabled")
suspend fun getMultipleYields(
@Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean,
@Query("type") type: YieldType,
@Query("revenueOption") revenueOption: RevenueOption,
@Query("page") page: Int,
@Query("network") network: String,
@Query("limit") limit: Int,
@Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean? = null,
@Query("type") type: YieldType? = null,
@Query("revenueOption") revenueOption: RevenueOption? = null,
@Query("page") page: Int? = null,
@Query("network") network: String? = null,
@Query("limit") limit: Int? = null,
): ApiResponse<EnabledYieldsResponse>
@GET("yields/{integrationId}")
suspend fun getSingleYield(
@Path("integrationId") integrationId: String,
@Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean = false,
): ApiResponse<Yield>
): ApiResponse<YieldDTO>
@GET("yields/balances")
suspend fun getMultipleYieldBalances(
@Body body: List<YieldBalanceRequestBody>,
): ApiResponse<List<YieldBalanceWrapper>>
): ApiResponse<List<YieldBalanceWrapperDTO>>
@GET("yields/{integrationId}/balances")
suspend fun getSingleYieldBalance(
@Path("integrationId") integrationId: String,
@Body body: YieldBalanceRequestBody,
): ApiResponse<YieldBalanceWrapper>
): ApiResponse<YieldBalanceWrapperDTO>
@GET("tokens")
suspend fun getTokens(): ApiResponse<List<TokenWithYield>>
suspend fun getTokens(): ApiResponse<List<TokenWithYieldDTO>>
@POST("actions/enter")
suspend fun createEnterAction(@Body body: EnterActionRequestBody): ApiResponse<EnterActionResponse>
@PATCH("transactions/{transactionId}")
suspend fun constructTransaction(
@Path("transactionId") transactionId: String,
@Body body: ConstructTransactionRequestBody,
): ApiResponse<StakingTransactionDTO>
@POST("transactions/{transactionId}/submit_hash")
suspend fun submitTransactionHash(
@Path("transactionId") transactionId: String,
@Body body: SubmitTransactionHashRequestBody,
): ApiResponse<Unit>
}

View file

@ -0,0 +1,39 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
data class Address(
@Json(name = "address")
val address: String,
@Json(name = "additionalAddresses")
val additionalAddresses: AdditionalAddresses? = null,
@Json(name = "explorerUrl")
val explorerUrl: String? = null,
) {
data class AdditionalAddresses(
// cosmos-specific
@Json(name = "cosmosPubKey")
val cosmosPubKey: String? = null,
// binance-specific
@Json(name = "binanceBeaconAddress")
val binanceBeaconAddress: String? = null,
// solana-specific
@Json(name = "stakeAccounts")
val stakeAccounts: List<String>? = null,
@Json(name = "lidoStakeAccounts")
val lidoStakeAccounts: List<String>? = null,
// tezos-specific
@Json(name = "tezosPubKey")
val tezosPubKey: String? = null,
// avalanche-specific
@Json(name = "cAddressBech")
val cAddressBech: String? = null,
@Json(name = "pAddressBech")
val pAddressBech: String? = null,
)
}

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
import java.math.BigDecimal
data class ConstructTransactionRequestBody(
@Json(name = "gasArgs")
val gasArgs: GasArgs? = null,
@Json(name = "ledgerWalletAPICompatible")
val ledgerWalletAPICompatible: Boolean? = null,
) {
data class GasArgs(
// cosmos-specific
@Json(name = "gasPrice")
val gasPrice: BigDecimal? = null,
// EVM eip 1559 specific
@Json(name = "type")
val type: Int? = null,
@Json(name = "maxFeePerGas")
val maxFeePerGas: BigDecimal? = null,
@Json(name = "maxPriorityFeePerGas")
val maxPriorityFeePerGas: BigDecimal? = null,
)
}

View file

@ -0,0 +1,47 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
data class EnterActionRequestBody(
@Json(name = "integrationId")
val integrationId: String,
@Json(name = "addresses")
val addresses: Address,
@Json(name = "args")
val args: EnterActionRequestBodyArgs,
@Json(name = "referralCode")
val referralCode: String? = null,
) {
data class EnterActionRequestBodyArgs(
@Json(name = "amount")
val amount: String,
@Json(name = "validatorAddress")
val validatorAddress: String? = null,
@Json(name = "validatorAddresses")
val validatorAddresses: List<String>? = null,
@Json(name = "providerId")
val providerId: String? = null,
@Json(name = "duration")
val duration: String? = null,
@Json(name = "nfts")
val nfts: List<YieldBalanceWrapperDTO.BalanceDTO.PendingAction.PendingActionArgs.Nft>? = null,
@Json(name = "ledgerWalletAPICompatible")
val ledgerWalletAPICompatible: Boolean? = null,
@Json(name = "tronResource")
val tronResource: String? = null,
@Json(name = "signatureVerification")
val signatureVerification: SignatureVerification? = null,
@Json(name = "inputToken")
val inputToken: TokenDTO? = null,
)
data class SignatureVerification(
@Json(name = "message")
val message: String,
@Json(name = "signed")
val signed: String,
)
}

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.api.stakekit.models.request
import com.squareup.moshi.Json
data class SubmitTransactionHashRequestBody(
@Json(name = "hash")
val hash: String,
)

View file

@ -8,23 +8,6 @@ data class YieldBalanceRequestBody(
@Json(name = "integrationId") val integrationId: String? = null,
) {
data class Address(
@Json(name = "address") val address: String,
@Json(name = "additionalAddresses") val additionalAddresses: AdditionalAddresses? = null,
@Json(name = "explorerUrl") val explorerUrl: String,
) {
data class AdditionalAddresses(
@Json(name = "cosmosPubKey") val cosmosPubKey: String? = null,
@Json(name = "binanceBeaconAddress") val binanceBeaconAddress: String? = null,
@Json(name = "stakeAccounts") val stakeAccounts: List<String>? = null,
@Json(name = "lidoStakeAccounts") val lidoStakeAccounts: List<String>? = null,
@Json(name = "tezosPubKey") val tezosPubKey: String? = null,
@Json(name = "cAddressBech") val cAddressBech: String? = null,
@Json(name = "pAddressBech") val pAddressBech: String? = null,
)
}
data class YieldBalanceRequestArgs(
@Json(name = "validatorAddresses") val validatorAddresses: List<String>,
)

View file

@ -2,12 +2,12 @@ package com.tangem.datasource.api.stakekit.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.stakekit.models.response.model.Yield
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
@JsonClass(generateAdapter = true)
data class EnabledYieldsResponse(
@Json(name = "data")
val data: List<Yield>,
val data: List<YieldDTO>,
@Json(name = "hasNextPage")
val hasNextPage: Boolean,
@Json(name = "limit")

View file

@ -0,0 +1,33 @@
package com.tangem.datasource.api.stakekit.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class EnterActionResponse(
@Json(name = "id")
val id: String,
@Json(name = "integrationId")
val integrationId: String,
@Json(name = "status")
val status: StakingActionStatusDTO,
@Json(name = "type")
val type: StakingActionTypeDTO,
@Json(name = "currentStepIndex")
val currentStepIndex: Int,
@Json(name = "amount")
val amount: BigDecimal,
@Json(name = "validatorAddress")
val validatorAddress: String?,
@Json(name = "validatorAddresses")
val validatorAddresses: List<String>?,
@Json(name = "transactions")
val transactions: List<StakingTransactionDTO>?,
@Json(name = "createdAt")
val createdAt: DateTime,
)

View file

@ -4,13 +4,13 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class AddressArgument(
data class AddressArgumentDTO(
@Json(name = "required")
val required: Boolean,
@Json(name = "network")
val network: String? = null,
@Json(name = "minimum")
val minimum: Int? = null,
val minimum: Double? = null,
@Json(name = "maximum")
val maximum: Int? = null,
val maximum: Double? = null,
)

View file

@ -0,0 +1,205 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
enum class NetworkTypeDTO {
@Json(name = "avalanche-c")
AVALANCHE_C,
@Json(name = "avalanche-atomic")
AVALANCHE_ATOMIC,
@Json(name = "avalanche-p")
AVALANCHE_P,
@Json(name = "arbitrum")
ARBITRUM,
@Json(name = "binance")
BINANCE,
@Json(name = "celo")
CELO,
@Json(name = "ethereum")
ETHEREUM,
@Json(name = "ethereum-goerli")
ETHEREUM_GOERLI,
@Json(name = "ethereum-holesky")
ETHEREUM_HOLESKY,
@Json(name = "fantom")
FANTOM,
@Json(name = "harmony")
HARMONY,
@Json(name = "optimism")
OPTIMISM,
@Json(name = "polygon")
POLYGON,
@Json(name = "gnosis")
GNOSIS,
@Json(name = "moonriver")
MOONRIVER,
@Json(name = "okc")
OKC,
@Json(name = "zksync")
ZKSYNC,
@Json(name = "viction")
VICTION,
@Json(name = "agoric")
AGORIC,
@Json(name = "akash")
AKASH,
@Json(name = "axelar")
AXELAR,
@Json(name = "band-protocol")
BAND_PROTOCOL,
@Json(name = "bitsong")
BITSONG,
@Json(name = "canto")
CANTO,
@Json(name = "chihuahua")
CHIHUAHUA,
@Json(name = "comdex")
COMDEX,
@Json(name = "coreum")
COREUM,
@Json(name = "cosmos")
COSMOS,
@Json(name = "crescent")
CRESCENT,
@Json(name = "cronos")
CRONOS,
@Json(name = "cudos")
CUDOS,
@Json(name = "desmos")
DESMOS,
@Json(name = "dydx")
DYDX,
@Json(name = "evmos")
EVMOS,
@Json(name = "fetch-ai")
FETCH_AI,
@Json(name = "gravity-bridge")
GRAVITY_BRIDGE,
@Json(name = "injective")
INJECTIVE,
@Json(name = "irisnet")
IRISNET,
@Json(name = "juno")
JUNO,
@Json(name = "kava")
KAVA,
@Json(name = "ki-network")
KI_NETWORK,
@Json(name = "mars-protocol")
MARS_PROTOCOL,
@Json(name = "nym")
NYM,
@Json(name = "okex-chain")
OKEX_CHAIN,
@Json(name = "onomy")
ONOMY,
@Json(name = "osmosis")
OSMOSIS,
@Json(name = "persistence")
PERSISTENCE,
@Json(name = "quicksilver")
QUICKSILVER,
@Json(name = "regen")
REGEN,
@Json(name = "secret")
SECRET,
@Json(name = "sentinel")
SENTINEL,
@Json(name = "sommelier")
SOMMELIER,
@Json(name = "stafi")
STAFI,
@Json(name = "stargaze")
STARGAZE,
@Json(name = "stride")
STRIDE,
@Json(name = "teritori")
TERITORI,
@Json(name = "tgrade")
TGRADE,
@Json(name = "umee")
UMEE,
@Json(name = "polkadot")
POLKADOT,
@Json(name = "kusama")
KUSAMA,
@Json(name = "westend")
WESTEND,
@Json(name = "binancebeacon")
BINANCEBEACON,
@Json(name = "near")
NEAR,
@Json(name = "solana")
SOLANA,
@Json(name = "tezos")
TEZOS,
@Json(name = "tron")
TRON,
UNKNOWN,
}

View file

@ -1,218 +0,0 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Token(
@Json(name = "name") val name: String,
@Json(name = "network") val network: NetworkType,
@Json(name = "symbol") val symbol: String,
@Json(name = "decimals") val decimals: Int,
@Json(name = "address") val address: String?,
@Json(name = "coinGeckoId") val coinGeckoId: String?,
@Json(name = "logoURI") val logoURI: String?,
@Json(name = "isPoints") val isPoints: Boolean?,
) {
enum class NetworkType {
@Json(name = "avalanche-c")
AVALANCHE_C,
@Json(name = "avalanche-atomic")
AVALANCHE_ATOMIC,
@Json(name = "avalanche-p")
AVALANCHE_P,
@Json(name = "arbitrum")
ARBITRUM,
@Json(name = "binance")
BINANCE,
@Json(name = "celo")
CELO,
@Json(name = "ethereum")
ETHEREUM,
@Json(name = "ethereum-goerli")
ETHEREUM_GOERLI,
@Json(name = "ethereum-holesky")
ETHEREUM_HOLESKY,
@Json(name = "fantom")
FANTOM,
@Json(name = "harmony")
HARMONY,
@Json(name = "optimism")
OPTIMISM,
@Json(name = "polygon")
POLYGON,
@Json(name = "gnosis")
GNOSIS,
@Json(name = "moonriver")
MOONRIVER,
@Json(name = "okc")
OKC,
@Json(name = "zksync")
ZKSYNC,
@Json(name = "viction")
VICTION,
@Json(name = "agoric")
AGORIC,
@Json(name = "akash")
AKASH,
@Json(name = "axelar")
AXELAR,
@Json(name = "band-protocol")
BAND_PROTOCOL,
@Json(name = "bitsong")
BITSONG,
@Json(name = "canto")
CANTO,
@Json(name = "chihuahua")
CHIHUAHUA,
@Json(name = "comdex")
COMDEX,
@Json(name = "coreum")
COREUM,
@Json(name = "cosmos")
COSMOS,
@Json(name = "crescent")
CRESCENT,
@Json(name = "cronos")
CRONOS,
@Json(name = "cudos")
CUDOS,
@Json(name = "desmos")
DESMOS,
@Json(name = "dydx")
DYDX,
@Json(name = "evmos")
EVMOS,
@Json(name = "fetch-ai")
FETCH_AI,
@Json(name = "gravity-bridge")
GRAVITY_BRIDGE,
@Json(name = "injective")
INJECTIVE,
@Json(name = "irisnet")
IRISNET,
@Json(name = "juno")
JUNO,
@Json(name = "kava")
KAVA,
@Json(name = "ki-network")
KI_NETWORK,
@Json(name = "mars-protocol")
MARS_PROTOCOL,
@Json(name = "nym")
NYM,
@Json(name = "okex-chain")
OKEX_CHAIN,
@Json(name = "onomy")
ONOMY,
@Json(name = "osmosis")
OSMOSIS,
@Json(name = "persistence")
PERSISTENCE,
@Json(name = "quicksilver")
QUICKSILVER,
@Json(name = "regen")
REGEN,
@Json(name = "secret")
SECRET,
@Json(name = "sentinel")
SENTINEL,
@Json(name = "sommelier")
SOMMELIER,
@Json(name = "stafi")
STAFI,
@Json(name = "stargaze")
STARGAZE,
@Json(name = "stride")
STRIDE,
@Json(name = "teritori")
TERITORI,
@Json(name = "tgrade")
TGRADE,
@Json(name = "umee")
UMEE,
@Json(name = "polkadot")
POLKADOT,
@Json(name = "kusama")
KUSAMA,
@Json(name = "westend")
WESTEND,
@Json(name = "binancebeacon")
BINANCEBEACON,
@Json(name = "near")
NEAR,
@Json(name = "solana")
SOLANA,
@Json(name = "tezos")
TEZOS,
@Json(name = "tron")
TRON,
UNKNOWN,
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TokenDTO(
@Json(name = "name")
val name: String,
@Json(name = "network")
val network: NetworkTypeDTO,
@Json(name = "symbol")
val symbol: String,
@Json(name = "decimals")
val decimals: Int,
@Json(name = "address")
val address: String?,
@Json(name = "coinGeckoId")
val coinGeckoId: String?,
@Json(name = "logoURI")
val logoURI: String?,
@Json(name = "isPoints")
val isPoints: Boolean?,
)

View file

@ -4,7 +4,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class TokenWithYield(
@Json(name = "token") val token: Token,
data class TokenWithYieldDTO(
@Json(name = "token") val token: TokenDTO,
@Json(name = "availableYields") val availableYieldIds: List<String>,
)

View file

@ -2,19 +2,20 @@ package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldBalanceWrapper(
data class YieldBalanceWrapperDTO(
@Json(name = "balances")
val balances: List<Balance>,
val balances: List<BalanceDTO>,
@Json(name = "integrationId")
val integrationId: String?,
) {
@JsonClass(generateAdapter = true)
data class Balance(
data class BalanceDTO(
@Json(name = "groupId")
val groupId: String,
@Json(name = "type")
@ -28,7 +29,7 @@ data class YieldBalanceWrapper(
@Json(name = "pendingActions")
val pendingActions: List<PendingAction>,
@Json(name = "token")
val token: Token,
val tokenDTO: TokenDTO,
@Json(name = "validatorAddress")
val validatorAddress: String?,
@Json(name = "validatorAddresses")
@ -61,12 +62,14 @@ data class YieldBalanceWrapper(
@Json(name = "unlocking")
UNLOCKING,
UNKNOWN,
}
@JsonClass(generateAdapter = true)
data class PendingAction(
@Json(name = "type")
val type: StakingActionType,
val type: StakingActionTypeDTO,
@Json(name = "passthrough")
val passthrough: String,
@Json(name = "args")

View file

@ -1,138 +0,0 @@
package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.joda.time.DateTime
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class YieldBalances(
@Json(name = "balances")
val balances: List<Balance>,
@Json(name = "integrationId")
val integrationId: String,
) {
@JsonClass(generateAdapter = true)
data class Balance(
@Json(name = "groupId")
val groupId: String,
@Json(name = "type")
val type: BalanceType,
@Json(name = "amount")
val amount: BigDecimal,
@Json(name = "date")
val date: DateTime?,
@Json(name = "pricePerShare")
val pricePerShare: BigDecimal,
@Json(name = "pendingActions")
val pendingActions: List<PendingAction>,
@Json(name = "token")
val token: Token,
@Json(name = "validatorAddress")
val validatorAddress: String?,
@Json(name = "validatorAddresses")
val validatorAddresses: List<String>?,
@Json(name = "providerId")
val providerId: String?,
) {
enum class BalanceType {
@Json(name = "available")
AVAILABLE,
@Json(name = "staked")
STAKED,
@Json(name = "unstaking")
UNSTAKING,
@Json(name = "unstaked")
UNSTAKED,
@Json(name = "preparing")
PREPARING,
@Json(name = "rewards")
REWARDS,
@Json(name = "locked")
LOCKED,
@Json(name = "unlocking")
UNLOCKING,
}
@JsonClass(generateAdapter = true)
data class PendingAction(
@Json(name = "type")
val type: StakingActionType,
@Json(name = "passthrough")
val passthrough: String,
@Json(name = "args")
val args: PendingActionArgs?,
) {
@JsonClass(generateAdapter = true)
data class PendingActionArgs(
@Json(name = "amount")
val amount: Amount?,
@Json(name = "duration")
val duration: Duration?,
@Json(name = "validatorAddress")
val validatorAddress: Required?,
@Json(name = "validatorAddresses")
val validatorAddresses: Required?,
@Json(name = "nfts")
val nfts: List<Nft>?,
@Json(name = "tronResource")
val tronResource: TronResource?,
@Json(name = "signatureVerification")
val signatureVerification: Required?,
) {
@JsonClass(generateAdapter = true)
data class Amount(
@Json(name = "required")
val required: Boolean,
@Json(name = "minimum")
val minimum: BigDecimal?,
@Json(name = "maximum")
val maximum: BigDecimal?,
)
@JsonClass(generateAdapter = true)
data class Duration(
@Json(name = "required")
val required: Boolean,
@Json(name = "minimum")
val minimum: Int?,
@Json(name = "maximum")
val maximum: Int?,
)
@JsonClass(generateAdapter = true)
data class Nft(
@Json(name = "baycId")
val baycId: Required?,
@Json(name = "maycId")
val maycId: Required?,
@Json(name = "bakcId")
val bakcId: Required?,
)
@JsonClass(generateAdapter = true)
data class TronResource(
@Json(name = "required")
val required: Boolean,
@Json(name = "options")
val options: List<String>,
)
}
}
@JsonClass(generateAdapter = true)
data class Required(
@Json(name = "required")
val required: Boolean,
)
}
}

View file

@ -5,33 +5,33 @@ import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class Yield(
data class YieldDTO(
@Json(name = "id")
val id: String,
@Json(name = "token")
val token: Token,
val token: TokenDTO,
@Json(name = "tokens")
val tokens: List<Token>,
val tokens: List<TokenDTO>,
@Json(name = "args")
val args: Args,
val args: ArgsDTO,
@Json(name = "status")
val status: Status,
val status: StatusDTO,
@Json(name = "apy")
val apy: BigDecimal,
@Json(name = "rewardRate")
val rewardRate: Double,
@Json(name = "rewardType")
val rewardType: RewardType,
val rewardType: RewardTypeDTO,
@Json(name = "metadata")
val metadata: Metadata,
val metadata: MetadataDTO,
@Json(name = "validators")
val validators: List<Validator>,
val validators: List<ValidatorDTO>,
@Json(name = "isAvailable")
val isAvailable: Boolean,
) {
@JsonClass(generateAdapter = true)
data class Status(
data class StatusDTO(
@Json(name = "enter")
val enter: Boolean,
@Json(name = "exit")
@ -39,7 +39,7 @@ data class Yield(
)
@JsonClass(generateAdapter = true)
data class Args(
data class ArgsDTO(
@Json(name = "enter")
val enter: Enter,
@Json(name = "exit")
@ -50,20 +50,20 @@ data class Yield(
@Json(name = "addresses")
val addresses: Addresses,
@Json(name = "args")
val args: Map<String, AddressArgument>,
val args: Map<String, AddressArgumentDTO>,
) {
@JsonClass(generateAdapter = true)
data class Addresses(
@Json(name = "address")
val address: AddressArgument,
val address: AddressArgumentDTO,
@Json(name = "additionalAddresses")
val additionalAddresses: Map<String, AddressArgument>? = null,
val additionalAddresses: Map<String, AddressArgumentDTO>? = null,
)
}
}
@JsonClass(generateAdapter = true)
data class Validator(
data class ValidatorDTO(
@Json(name = "address")
val address: String,
@Json(name = "status")
@ -75,7 +75,7 @@ data class Yield(
@Json(name = "website")
val website: String?,
@Json(name = "apr")
val apr: Double?,
val apr: BigDecimal?,
@Json(name = "commission")
val commission: Double?,
@Json(name = "stakedBalance")
@ -87,7 +87,7 @@ data class Yield(
)
@JsonClass(generateAdapter = true)
data class Metadata(
data class MetadataDTO(
@Json(name = "name")
val name: String,
@Json(name = "logoURI")
@ -97,19 +97,19 @@ data class Yield(
@Json(name = "documentation")
val documentation: String?,
@Json(name = "gasFeeToken")
val gasFeeToken: Token,
val gasFeeTokenDTO: TokenDTO,
@Json(name = "token")
val token: Token,
val tokenDTO: TokenDTO,
@Json(name = "tokens")
val tokens: List<Token>,
val tokensDTO: List<TokenDTO>,
@Json(name = "type")
val type: String,
@Json(name = "rewardSchedule")
val rewardSchedule: String,
@Json(name = "cooldownPeriod")
val cooldownPeriod: Period,
val cooldownPeriod: PeriodDTO,
@Json(name = "warmupPeriod")
val warmupPeriod: Period,
val warmupPeriod: PeriodDTO,
@Json(name = "rewardClaiming")
val rewardClaiming: String,
@Json(name = "defaultValidator")
@ -119,28 +119,30 @@ data class Yield(
@Json(name = "supportsMultipleValidators")
val supportsMultipleValidators: Boolean,
@Json(name = "revshare")
val revshare: Enabled,
val revshare: EnabledDTO,
@Json(name = "fee")
val fee: Enabled,
val fee: EnabledDTO,
) {
@JsonClass(generateAdapter = true)
data class Period(
data class PeriodDTO(
@Json(name = "days")
val days: Int,
)
@JsonClass(generateAdapter = true)
data class Enabled(
data class EnabledDTO(
@Json(name = "enabled")
val enabled: Boolean,
)
}
enum class RewardType {
enum class RewardTypeDTO {
@Json(name = "apy")
APY, // auto
APY, // compound rate
@Json(name = "apr")
APR, // manual
APR, // simple rate,
UNKNOWN,
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.datasource.api.stakekit.models.response.model.action
import com.squareup.moshi.Json
enum class StakingActionStatusDTO {
@Json(name = "CANCELED")
CANCELED,
@Json(name = "CREATED")
CREATED,
@Json(name = "WAITING_FOR_NEXT")
WAITING_FOR_NEXT,
@Json(name = "PROCESSING")
PROCESSING,
@Json(name = "FAILED")
FAILED,
@Json(name = "SUCCESS")
SUCCESS,
UNKNOWN,
}

View file

@ -1,8 +1,8 @@
package com.tangem.datasource.api.stakekit.models.response.model
package com.tangem.datasource.api.stakekit.models.response.model.action
import com.squareup.moshi.Json
enum class StakingActionType {
enum class StakingActionTypeDTO {
@Json(name = "STAKE")
STAKE,
@ -47,4 +47,6 @@ enum class StakingActionType {
@Json(name = "MIGRATE")
MIGRATE,
UNKNOWN,
}

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.api.stakekit.models.response.model.transaction
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class StakingGasEstimateDTO(
@Json(name = "amount")
val amount: BigDecimal,
@Json(name = "token")
val token: TokenDTO,
@Json(name = "gasLimit")
val gasLimit: BigDecimal,
)

View file

@ -0,0 +1,37 @@
package com.tangem.datasource.api.stakekit.models.response.model.transaction
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
@JsonClass(generateAdapter = true)
data class StakingTransactionDTO(
@Json(name = "id")
val id: String,
@Json(name = "network")
val network: NetworkTypeDTO,
@Json(name = "status")
val status: StakingTransactionStatusDTO,
@Json(name = "type")
val type: StakingTransactionTypeDTO,
@Json(name = "hash")
val hash: String?,
@Json(name = "signedTransaction")
val signedTransaction: String?,
@Json(name = "unsignedTransaction")
val unsignedTransaction: String?,
@Json(name = "stepIndex")
val stepIndex: Int,
@Json(name = "error")
val error: String?,
@Json(name = "gasEstimate")
val gasEstimate: StakingGasEstimateDTO?,
@Json(name = "stakeId")
val stakeId: String?,
@Json(name = "explorerUrl")
val explorerUrl: String?,
@Json(name = "ledgerHwAppId")
val ledgerHwAppId: String?,
@Json(name = "isMessage")
val isMessage: Boolean,
)

View file

@ -0,0 +1,37 @@
package com.tangem.datasource.api.stakekit.models.response.model.transaction
import com.squareup.moshi.Json
enum class StakingTransactionStatusDTO {
@Json(name = "NOT_FOUND")
NOT_FOUND,
@Json(name = "CREATED")
CREATED,
@Json(name = "BLOCKED")
BLOCKED,
@Json(name = "WAITING_FOR_SIGNATURE")
WAITING_FOR_SIGNATURE,
@Json(name = "SIGNED")
SIGNED,
@Json(name = "BROADCASTED")
BROADCASTED,
@Json(name = "PENDING")
PENDING,
@Json(name = "CONFIRMED")
CONFIRMED,
@Json(name = "FAILED")
FAILED,
@Json(name = "SKIPPED")
SKIPPED,
UNKNOWN,
}

View file

@ -0,0 +1,124 @@
package com.tangem.datasource.api.stakekit.models.response.model.transaction
import com.squareup.moshi.Json
enum class StakingTransactionTypeDTO {
@Json(name = "SWAP")
SWAP,
@Json(name = "DEPOSIT")
DEPOSIT,
@Json(name = "APPROVAL")
APPROVAL,
@Json(name = "STAKE")
STAKE,
@Json(name = "CLAIM_UNSTAKED")
CLAIM_UNSTAKED,
@Json(name = "CLAIM_REWARDS")
CLAIM_REWARDS,
@Json(name = "RESTAKE_REWARDS")
RESTAKE_REWARDS,
@Json(name = "UNSTAKE")
UNSTAKE,
@Json(name = "SPLIT")
SPLIT,
@Json(name = "MERGE")
MERGE,
@Json(name = "LOCK")
LOCK,
@Json(name = "UNLOCK")
UNLOCK,
@Json(name = "SUPPLY")
SUPPLY,
@Json(name = "BRIDGE")
BRIDGE,
@Json(name = "VOTE")
VOTE,
@Json(name = "REVOKE")
REVOKE,
@Json(name = "RESTAKE")
RESTAKE,
@Json(name = "REBOND")
REBOND,
@Json(name = "WITHDRAW")
WITHDRAW,
@Json(name = "CREATE_ACCOUNT")
CREATE_ACCOUNT,
@Json(name = "REVEAL")
REVEAL,
@Json(name = "MIGRATE")
MIGRATE,
@Json(name = "UTXO_P_TO_C_IMPORT")
UTXO_P_TO_C_IMPORT,
@Json(name = "UTXO_C_TO_P_IMPORT")
UTXO_C_TO_P_IMPORT,
@Json(name = "UNFREEZE_LEGACY")
UNFREEZE_LEGACY,
@Json(name = "UNFREEZE_LEGACY_BANDWIDTH")
UNFREEZE_LEGACY_BANDWIDTH,
@Json(name = "UNFREEZE_LEGACY_ENERGY")
UNFREEZE_LEGACY_ENERGY,
@Json(name = "UNFREEZE_BANDWIDTH")
UNFREEZE_BANDWIDTH,
@Json(name = "UNFREEZE_ENERGY")
UNFREEZE_ENERGY,
@Json(name = "FREEZE_BANDWIDTH")
FREEZE_BANDWIDTH,
@Json(name = "FREEZE_ENERGY")
FREEZE_ENERGY,
@Json(name = "UNDELEGATE_BANDWIDTH")
UNDELEGATE_BANDWIDTH,
@Json(name = "UNDELEGATE_ENERGY")
UNDELEGATE_ENERGY,
@Json(name = "P2P_NODE_REQUEST")
P2P_NODE_REQUEST,
@Json(name = "LUGANODES_PROVISION")
LUGANODES_PROVISION,
@Json(name = "LUGANODES_EXIT_REQUEST")
LUGANODES_EXIT_REQUEST,
@Json(name = "INFSTONES_PROVISION")
INFSTONES_PROVISION,
@Json(name = "INFSTONES_EXIT_REQUEST")
INFSTONES_EXIT_REQUEST,
@Json(name = "INFSTONES_CLAIM_REQUEST")
INFSTONES_CLAIM_REQUEST,
UNKNOWN,
}

View file

@ -1,8 +1,8 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.token.DefaultAssetsStore
import com.tangem.datasource.local.token.AssetsStore
import com.tangem.datasource.local.token.DefaultExpressAssetsStore
import com.tangem.datasource.local.token.ExpressAssetsStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -11,11 +11,11 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object AssetsStoreModule {
internal object ExpressAssetsStoreModule {
@Provides
@Singleton
fun provideAssetsStore(): AssetsStore {
return DefaultAssetsStore(dataStore = RuntimeDataStore())
fun provideExpressAssetsStore(): ExpressAssetsStore {
return DefaultExpressAssetsStore(dataStore = RuntimeDataStore())
}
}

View file

@ -4,11 +4,10 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.adapter.*
import com.tangem.datasource.api.common.adapter.BigDecimalAdapter
import com.tangem.datasource.api.common.adapter.DateTimeAdapter
import com.tangem.datasource.api.common.adapter.LocalDateAdapter
import com.tangem.datasource.api.common.adapter.UnknownEnumMoshiAdapter
import com.tangem.datasource.api.stakekit.models.response.model.Token
import com.tangem.datasource.config.models.ProviderModel
import dagger.Module
import dagger.Provides
@ -35,10 +34,7 @@ class MoshiModule {
.add(LocalDateAdapter())
.add(DateTimeAdapter())
.add(KotlinJsonAdapterFactory())
.add(
Token.NetworkType::class.java,
UnknownEnumMoshiAdapter.create(Token.NetworkType::class.java, Token.NetworkType.UNKNOWN),
)
.addStakeKitEnumFallbackAdapters()
.build()
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.token.DefaultStakingYieldsStore
import com.tangem.datasource.local.token.StakingYieldsStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object StakingTokensStoreModule {
@Provides
@Singleton
fun provideStakingTokensStore(): StakingYieldsStore {
return DefaultStakingYieldsStore(dataStore = RuntimeDataStore())
}
}

View file

@ -94,6 +94,20 @@ object PreferencesKeys {
val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") }
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
// region Permission
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")
fun getShouldShowInitialPermissionScreen(permission: String) =
booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission")
fun getIsFirstTimeAskingPermission(permission: String) =
booleanPreferencesKey("shouldAskInitialPushPermission_$permission")
fun getPermissionLaunchCount(permission: String) = intPreferencesKey("pushPermissionLaunchCount_$permission")
fun getPermissionDaysCount(permission: String) = longPreferencesKey("pushPermissionDaysCount_$permission")
// endregion
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */

View file

@ -4,9 +4,9 @@ import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.wallets.models.UserWalletId
internal class DefaultAssetsStore(
internal class DefaultExpressAssetsStore(
private val dataStore: StringKeyDataStore<List<Asset>>,
) : AssetsStore {
) : ExpressAssetsStore {
override suspend fun getSyncOrNull(userWalletId: UserWalletId): List<Asset>? {
return dataStore.getSyncOrNull(userWalletId.stringValue)

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.staking.model.StakingTokenWithYield
internal class DefaultStakingTokensStore(
private val dataStore: StringKeyDataStore<List<StakingTokenWithYield>>,
) : StakingTokensStore {
override suspend fun getSyncOrNull(): List<StakingTokenWithYield>? {
return dataStore.getSyncOrNull(STAKING_TOKENS_KEY)
}
override suspend fun store(items: List<StakingTokenWithYield>) {
dataStore.store(STAKING_TOKENS_KEY, items)
}
companion object {
private const val STAKING_TOKENS_KEY = "STAKING_TOKENS_KEY"
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
internal class DefaultStakingYieldsStore(
private val dataStore: StringKeyDataStore<List<YieldDTO>>,
) : StakingYieldsStore {
override suspend fun getSyncOrNull(): List<YieldDTO>? {
return dataStore.getSyncOrNull(STAKING_YIELDS_KEY)
}
override suspend fun store(items: List<YieldDTO>) {
dataStore.store(STAKING_YIELDS_KEY, items)
}
companion object {
private const val STAKING_YIELDS_KEY = "STAKING_YIELDS_KEY"
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.datasource.local.token
import com.tangem.datasource.api.express.models.response.Asset
import com.tangem.domain.wallets.models.UserWalletId
interface AssetsStore {
interface ExpressAssetsStore {
suspend fun getSyncOrNull(userWalletId: UserWalletId): List<Asset>?

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.local.token
import com.tangem.domain.staking.model.StakingTokenWithYield
interface StakingTokensStore {
suspend fun getSyncOrNull(): List<StakingTokenWithYield>?
suspend fun store(items: List<StakingTokenWithYield>)
}

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.local.token
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
interface StakingYieldsStore {
suspend fun getSyncOrNull(): List<YieldDTO>?
suspend fun store(items: List<YieldDTO>)
}

View file

@ -35,5 +35,6 @@ sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
class StakeKit(stakeKitAuthProvider: StakeKitAuthProvider) : RequestHeader(
"X-API-KEY" to { stakeKitAuthProvider.getApiKey() },
"accept" to { "application/json" },
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.core.decompose.di
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import dagger.BindsInstance
@ -37,6 +38,14 @@ interface DecomposeComponent {
*/
fun uiMessageSender(@BindsInstance uiMessageSender: UiMessageSender): Builder
/**
* Sets the parameters container for the component.
*
* @param container The parameters container to set.
* @return The builder instance.
*/
fun paramsContainer(@BindsInstance container: ParamsContainer): Builder
/**
* Builds the Decompose component.
*

View file

@ -0,0 +1,8 @@
package com.tangem.core.decompose.factory
import com.tangem.core.decompose.context.AppComponentContext
interface ComponentFactory<P : Any, C : Any> {
fun create(context: AppComponentContext, params: P): C
}

View file

@ -22,24 +22,43 @@ interface ModelsEntryPoint {
}
/**
* Gets or creates a component's [Model].
* Gets or creates a component's [Model] with no parameters.
*/
inline fun <reified M : Model> AppComponentContext.getOrCreateModel(): M {
val modelKey = "model_${M::class.simpleName}"
inline fun <reified M : Model> AppComponentContext.getOrCreateModel(): M = getOrCreateModel(params = null)
/**
* Gets or creates a component's [Model].
*
* Be careful with objects you pass in parameters as they will be used in [Model] lifecycle.
* If you pass a object with different lifecycle than the [Model] then you can face memory leaks.
*
* @param params The parameters to store in the [ParamsContainer],
*/
inline fun <reified M : Model, reified P : Any> AppComponentContext.getOrCreateModel(params: P?): M {
val entryPoint = instanceKeeper.getOrCreateSimple(key = "modelsEntryPoint") {
val hiltComponent = hiltComponentBuilder
.router(router)
.uiMessageSender(messageSender)
.let { builder ->
if (params != null) {
val container = MutableParamsContainer(params)
builder.paramsContainer(container)
} else {
builder
}
}
.build()
EntryPoints.get(hiltComponent, ModelsEntryPoint::class.java)
}
val modelKey = "model_${M::class.simpleName}"
val model = instanceKeeper.getOrCreate(modelKey) {
requireNotNull(entryPoint.models()[M::class.java]?.get()) {
"Model ${M::class.simpleName} is not provided"
}
} as M
}
val isModelExist = tags.getOrElse(modelKey) { false } as Boolean
@ -47,5 +66,5 @@ inline fun <reified M : Model> AppComponentContext.getOrCreateModel(): M {
tags[modelKey] = true
}
return model as M
return model
}

View file

@ -0,0 +1,45 @@
package com.tangem.core.decompose.model
/**
* Lazy container for [Model] params.
*
* This contrainer can be accessed by DI because it's provided via [com.tangem.core.decompose.di.DecomposeComponent].
* */
interface ParamsContainer {
/** Returns stored value if it is of type [T], otherwise returns null. */
fun <T> get(): T?
/** Returns stored value if it is of type [T], otherwise throws an exception. */
fun <T> require(): T
}
/**
* Mutable implementation of [ParamsContainer].
*
* ***You should not use this class directly in other modules, use immutable [ParamsContainer] instead.***
* */
class MutableParamsContainer private constructor() : ParamsContainer {
private var value: Any? = null
/** Stores [value] inside a container, replaces any previous stored value. */
fun set(value: Any) {
this.value = value
}
@Suppress("UNCHECKED_CAST")
override fun <T> get(): T? = value as? T
override fun <T> require(): T = get() ?: error("Contrainer is empty or contains a value of a different type.")
companion object {
/** Creates a new [MutableParamsContainer] and stores [value] inside it. */
operator fun <T : Any> invoke(value: T): MutableParamsContainer {
return MutableParamsContainer().apply {
set(value)
}
}
}
}

View file

@ -6,6 +6,7 @@ import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.popWhile
import com.arkivanov.decompose.router.stack.pushNew
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import kotlin.reflect.KClass
internal class DefaultRouter(
private val navigationProvider: AppNavigationProvider,
@ -19,6 +20,15 @@ internal class DefaultRouter(
navigation.pushNew(route, onComplete)
}
override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) {
val newRoutes = routes.toList()
navigation.navigate(
transformer = { newRoutes },
onComplete = { newStack, _ -> onComplete(newStack.size == newRoutes.size) },
)
}
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
navigation.pop(onComplete)
}
@ -29,4 +39,11 @@ internal class DefaultRouter(
onComplete = onComplete,
)
}
override fun popTo(routeClass: KClass<out Route>, onComplete: (isSuccess: Boolean) -> Unit) {
navigation.popWhile(
predicate = { it::class != routeClass },
onComplete = onComplete,
)
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.core.decompose.navigation
import kotlin.reflect.KClass
class DummyRouter : Router {
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
onComplete(true)
}
override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) {
onComplete(true)
}
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
onComplete(true)
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
onComplete(true)
}
override fun popTo(routeClass: KClass<out Route>, onComplete: (isSuccess: Boolean) -> Unit) {
onComplete(true)
}
}

View file

@ -1,8 +1,10 @@
package com.tangem.core.decompose.navigation
import kotlin.reflect.KClass
/**
* Interface for a router in the application.
* It provides methods for navigating through the application.
* It provides methods for navigating through the application by stack.
*/
interface Router {
@ -14,6 +16,14 @@ interface Router {
*/
fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Replaces ***all*** routes in the navigation stack with the specified [routes].
*
* @param routes The routes to replace the current stack with.
* @param onComplete The callback to be invoked when the operation is complete.
*/
fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Pops the top route from the navigation stack.
*
@ -22,10 +32,18 @@ interface Router {
fun pop(onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Pops routes from the navigation stack until the specified route is found.
* Pops routes from the navigation stack until the specified [route] is found.
*
* @param route The route to pop to.
* @param onComplete The callback to be invoked when the operation is complete.
*/
fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
/**
* Pops routes from the navigation stack until the ***first*** specified [routeClass] is found.
*
* @param routeClass The route class to pop to.
* @param onComplete The callback to be invoked when the operation is complete.
*/
fun popTo(routeClass: KClass<out Route>, onComplete: (isSuccess: Boolean) -> Unit = {})
}

View file

@ -0,0 +1,11 @@
package com.tangem.core.decompose.navigation
/**
* Pops routes from the navigation stack until the specified route [R] is found.
*
* @param R The route to pop to.
* @param onComplete The callback to be invoked when the operation is complete.
*/
inline fun <reified R : Route> Router.popTo(noinline onComplete: (isSuccess: Boolean) -> Unit = {}) {
popTo(R::class, onComplete)
}

View file

@ -42,5 +42,9 @@
{
"name": "DETAILS_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "PUSH_NOTIFICATIONS_ENABLED",
"version": "undefined"
}
]

View file

@ -1,33 +0,0 @@
package com.tangem.core.navigation
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.rekotlin.Action
import java.lang.ref.WeakReference
sealed class NavigationAction : Action {
data class NavigateTo(
val screen: AppScreen,
val fragmentShareTransition: FragmentShareTransition? = null,
val addToBackstack: Boolean = true,
val bundle: Bundle? = null,
) : NavigationAction()
data class PopBackTo(val screen: AppScreen? = null, val inclusive: Boolean = false) : NavigationAction()
data class OpenUrl(val url: String) : NavigationAction()
data class OpenDocument(val url: Uri) : NavigationAction()
object OpenBiometricsSettings : NavigationAction()
data class OpenDialog(val stateDialog: StateDialog) : NavigationAction()
data class Share(val data: String) : NavigationAction()
data class ActivityCreated(val activity: WeakReference<AppCompatActivity>) : NavigationAction()
data class ActivityDestroyed(val activity: WeakReference<AppCompatActivity>) : NavigationAction()
}

View file

@ -1,38 +0,0 @@
package com.tangem.core.navigation
import androidx.appcompat.app.AppCompatActivity
import org.rekotlin.StateType
import java.lang.ref.WeakReference
data class NavigationState(
val backStack: List<AppScreen> = emptyList(),
val activity: WeakReference<AppCompatActivity>? = null,
) : StateType
enum class AppScreen(val isDialogFragment: Boolean = false) {
Home,
Disclaimer,
OnboardingNote,
OnboardingWallet,
OnboardingTwins,
OnboardingOther,
Wallet,
WalletDetails,
Send(isDialogFragment = true),
Details,
DetailsSecurity,
CardSettings,
AppSettings,
ResetToFactory,
AccessCodeRecovery,
ManageTokens,
AddCustomToken,
WalletConnectSessions,
QrScanning,
ReferralProgram,
Swap,
Welcome,
SaveWallet(isDialogFragment = true),
AppCurrencySelector,
ModalNotification(isDialogFragment = true),
}

View file

@ -1,16 +0,0 @@
package com.tangem.core.navigation
/**
* Navigation controller that based on redux actions
*
[REDACTED_AUTHOR]
*/
interface ReduxNavController {
/** Navigate by [action] */
fun navigate(action: NavigationAction)
fun popBackStack(screen: AppScreen? = null)
fun getBackStack(): List<AppScreen>
}

View file

@ -1,18 +1,8 @@
package com.tangem.core.navigation
import android.view.View
import androidx.transition.TransitionSet
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
*/
data class FragmentShareTransition(
val shareElements: List<ShareElement>,
val enterTransitionSet: TransitionSet,
val exitTransitionSet: TransitionSet,
)
/**
* For ease of use, the name is used as transitionName\name into the FragmentTransaction.addSharedElement
*/

View file

@ -1,10 +0,0 @@
package com.tangem.core.navigation
interface StateDialog {
data class ScanFailsDialog(val source: ScanFailsSource) : StateDialog
enum class ScanFailsSource {
MAIN, SIGN_IN, SETTINGS, INTRO;
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.navigation.feedback
class DummyFeedbackManager : FeedbackManager {
override fun sendEmail(type: FeedbackType) {
/* no-op */
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.navigation.feedback
interface FeedbackManager {
fun sendEmail(type: FeedbackType)
}

View file

@ -0,0 +1,13 @@
package com.tangem.core.navigation.feedback
sealed class FeedbackType {
data object RateCanBeBetter : FeedbackType()
data object ScanFails : FeedbackType()
data class SendTransactionFailed(val error: String) : FeedbackType()
data object Feedback : FeedbackType()
data object Support : FeedbackType()
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.navigation.finisher
interface AppFinisher {
fun finish()
fun restart()
}

View file

@ -0,0 +1,5 @@
package com.tangem.core.navigation.settings
class DummySettingsManager : SettingsManager {
override fun openSettings() { /* no-op */ }
}

View file

@ -0,0 +1,5 @@
package com.tangem.core.navigation.settings
interface SettingsManager {
fun openSettings()
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.navigation.share
class DummyShareManager : ShareManager {
override fun shareText(text: String) {
/* no-op */
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.navigation.share
interface ShareManager {
fun shareText(text: String)
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.navigation.url
class DummyUrlOpener : UrlOpener {
override fun openUrl(url: String) {
/* no-op */
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.navigation.url
interface UrlOpener {
fun openUrl(url: String)
}

View file

@ -1,9 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="add_custom_token_choose_network">Выберите сеть</string>
<string name="add_custom_token_title">Добавить токен</string>
<string name="add_tokens_title">Валюты</string>
<string name="address_qr_code_message_format">Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств.</string>
<string name="alert_button_how_to_scan">Как сканировать</string>
<string name="alert_button_request_support">Обратиться в поддержку</string>
<string name="alert_button_try_again">Попробовать снова</string>
<string name="alert_demo_feature_disabled">Эта функция недоступна в демонстрационном режиме</string>
<string name="alert_failed_to_send_transaction_message">Причина: %s</string>
<string name="alert_failed_to_send_transaction_title">Не могу отправить транзакцию</string>
@ -81,6 +84,7 @@
<string name="common_copy">Копировать</string>
<string name="common_copy_address">Скопировать адрес</string>
<string name="common_create">Создать</string>
<string name="common_custom">Свое</string>
<string name="common_delete">Удалить</string>
<string name="common_disabled">Отключено</string>
<string name="common_done">Готово</string>
@ -92,7 +96,6 @@
<string name="common_explorer">Обозреватель</string>
<string name="common_fee_label">Комиссия</string>
<string name="common_fee_selector_footer">Сетевые комиссии это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s</string>
<string name="common_fee_selector_option_custom">Свое</string>
<string name="common_fee_selector_option_fast">Быстро</string>
<string name="common_fee_selector_option_market">По рынку</string>
<string name="common_fee_selector_option_slow">Медленно</string>
@ -117,6 +120,7 @@
<string name="common_reject">Отклонить</string>
<string name="common_reload">Перезагрузить</string>
<string name="common_rename">Переименовать</string>
<string name="common_save">Сохранить</string>
<string name="common_save_changes">Сохранить изменения</string>
<string name="common_search">Искать</string>
<string name="common_search_tokens">Поиск токенов</string>
@ -128,6 +132,7 @@
<string name="common_share">Поделиться</string>
<string name="common_sign">Подписать</string>
<string name="common_sign_and_send">Подписать и отправить</string>
<string name="common_staking">Стейкинг</string>
<string name="common_start">Начать</string>
<string name="common_submit">Отправить</string>
<string name="common_success">Успешно</string>
@ -169,6 +174,7 @@
<string name="custom_token_validation_error_not_found">Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить</string>
<string name="custom_token_validation_error_not_found_description">Остерегайтесь мошеннических токенов, они могут ничего не стоить</string>
<string name="custom_token_validation_error_not_found_title">Токены могут быть созданы кем угодно</string>
<string name="details_buy_wallet">Купить кошелек Tangem</string>
<string name="details_chat">Чат</string>
<string name="details_manage_security_access_code">Код доступа</string>
<string name="details_manage_security_access_code_description">Перед сканированием карты вам нужно будет ввести правильный код доступа.</string>
@ -186,6 +192,7 @@
<string name="details_row_title_flip_to_hide">Скрывать балансы жестом переворота</string>
<string name="details_row_title_issuer">Эмитент</string>
<string name="details_row_title_signed_hashes">Подписано</string>
<string name="details_send_feedback">Отправить отзыв</string>
<string name="details_title">Подробности</string>
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
<string name="disclaimer_title">Условия использования</string>
@ -295,7 +302,9 @@
<item quantity="many">%1$d из %2$d кошельков</item>
<item quantity="other">%1$d из %2$d кошельков</item>
</plurals>
<string name="manage_tokens_remove">Удалить</string>
<string name="manage_tokens_search_placeholder">например Bitcoin</string>
<string name="manage_tokens_toast_portfolio_updated">Ваш портфель был обновлен</string>
<string name="manage_tokens_unavailable_description">Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.</string>
<string name="manage_tokens_unavailable_vote">Голосовать</string>
<string name="manage_tokens_wallet_selector_title">Выберите кошелек</string>
@ -535,9 +544,12 @@
<string name="send_transaction_success">Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время</string>
<string name="send_validation_invalid_address">Неверный адрес</string>
<string name="sent_transaction_sent_title">Транзакция отправлена</string>
<string name="settings_card_settings_footer">Подготовьтесь к сканированию карты, которую вы хотите настроить.</string>
<string name="settings_forget_wallet">Забыть кошелек</string>
<string name="settings_forget_wallet_footer">Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова.</string>
<string name="settings_wallet_name_title">Имя</string>
<string name="staking_details_available">Доступно</string>
<string name="staking_details_title">Стейкинг %s</string>
<string name="story_awe_description">Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.</string>
<string name="story_awe_title">Революционный аппаратный кошелек</string>
<string name="story_backup_description">До трех карт с одним кошельком</string>
@ -579,9 +591,10 @@
<string name="token_button_unavailability_reason_empty_balance_sell">У вас нет средств для продажи. Пополните счет, чтобы иметь возможность продать с него средства.</string>
<string name="token_button_unavailability_reason_empty_balance_send">У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства.</string>
<string name="token_button_unavailability_reason_not_exchangeable">В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями.</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">Продажа средств станет доступной после завершения транзакции(-ий) в сети %s</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">Продажа средств станет доступной после завершения транзакции(-ий) в сети %s</string>
<string name="token_button_unavailability_reason_pending_transaction_send">Отправка средств станет доступной после завершения транзакции(-ий) в сети %s</string>
<string name="token_button_unavailability_reason_sell_unavailable">В данный момент продажа %s недоступна. Следите за нашими обновлениями.</string>
<string name="token_button_unavailability_reason_staking_unavailable">В данный момент стейкинг монеты %s недоступен. Следите за нашими обновлениями.</string>
<string name="token_details_generate_xpub">Сгенерировать XPUB</string>
<string name="token_details_hide_alert_hide">Скрыть</string>
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.</string>

View file

@ -1,9 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="add_custom_token_choose_network">Choose network</string>
<string name="add_custom_token_title">Add custom token</string>
<string name="add_tokens_title">Manage tokens</string>
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
<string name="alert_button_how_to_scan">How to scan</string>
<string name="alert_button_request_support">Request support</string>
<string name="alert_button_try_again">Try again</string>
<string name="alert_demo_feature_disabled">This feature is disabled in Demo mode</string>
<string name="alert_failed_to_send_transaction_message">Reason: %s</string>
<string name="alert_failed_to_send_transaction_title">Can\'t send a transaction</string>
@ -64,6 +67,8 @@
<string name="cardano_max_amount_has_token_title">Not enough ADA</string>
<string name="common_accept">Accept</string>
<string name="common_access_denied">Access denied</string>
<string name="common_all">All</string>
<string name="common_allow">Allow</string>
<string name="common_apply">Apply</string>
<string name="common_approval">Approval</string>
<string name="common_attention">Attention</string>
@ -80,6 +85,7 @@
<string name="common_copy">Copy</string>
<string name="common_copy_address">Copy address</string>
<string name="common_create">Create</string>
<string name="common_custom">Custom</string>
<string name="common_delete">Delete</string>
<string name="common_disabled">Disabled</string>
<string name="common_done">Done</string>
@ -91,7 +97,6 @@
<string name="common_explorer">Explorer</string>
<string name="common_fee_label">Fee</string>
<string name="common_fee_selector_footer">Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s</string>
<string name="common_fee_selector_option_custom">Custom</string>
<string name="common_fee_selector_option_fast">Fast</string>
<string name="common_fee_selector_option_market">Market</string>
<string name="common_fee_selector_option_slow">Slow</string>
@ -111,11 +116,13 @@
<string name="common_origin_card">Primary Card</string>
<string name="common_passphrase">Passphrase</string>
<string name="common_paste">Paste</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_read_more">Read more</string>
<string name="common_receive">Receive</string>
<string name="common_reject">Reject</string>
<string name="common_reload">Reload</string>
<string name="common_rename">Rename</string>
<string name="common_save">Save</string>
<string name="common_save_changes">Save changes</string>
<string name="common_search">Search</string>
<string name="common_search_tokens">Search tokens</string>
@ -127,6 +134,8 @@
<string name="common_share">Share</string>
<string name="common_sign">Sign</string>
<string name="common_sign_and_send">Sign and send</string>
<string name="common_stake">Stake</string>
<string name="common_staking">Staking</string>
<string name="common_start">Start</string>
<string name="common_submit">Submit</string>
<string name="common_success">Success</string>
@ -168,6 +177,7 @@
<string name="custom_token_validation_error_not_found">Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing.</string>
<string name="custom_token_validation_error_not_found_description">Be aware of adding scam tokens, they can cost nothing</string>
<string name="custom_token_validation_error_not_found_title">Note that tokens can be created by anyone</string>
<string name="details_buy_wallet">Buy Tangem Wallet</string>
<string name="details_chat">Chat</string>
<string name="details_manage_security_access_code">Access code</string>
<string name="details_manage_security_access_code_description">You will have to submit the correct access code before scanning the card</string>
@ -185,6 +195,7 @@
<string name="details_row_title_flip_to_hide">Flip-to-Hide Balances</string>
<string name="details_row_title_issuer">Issuer</string>
<string name="details_row_title_signed_hashes">Signed</string>
<string name="details_send_feedback">Send feedback</string>
<string name="details_title">Details</string>
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
<string name="disclaimer_title">Terms of service</string>
@ -294,11 +305,20 @@
<item quantity="one">%1$d of %2$d wallet</item>
<item quantity="other">%1$d of %2$d wallets</item>
</plurals>
<string name="manage_tokens_remove">Remove</string>
<string name="manage_tokens_search_placeholder">e.g. BTC I trust, hodl I must</string>
<string name="manage_tokens_toast_portfolio_updated">Your portfolio has been updated</string>
<string name="manage_tokens_unavailable_description">The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it.</string>
<string name="manage_tokens_unavailable_vote">Upvote</string>
<string name="manage_tokens_wallet_selector_title">Choose wallet</string>
<string name="manage_tokens_wallet_support_only_one_network_title">The wallet doesn\'t support more than one network</string>
<string name="markets_add_to_my_portfolio_description">To start buying, exchanging or receiving this asset, add this token to at least 1 network</string>
<string name="markets_add_to_my_portfolio_unavailable_description">This asset is not available</string>
<string name="markets_add_to_portfolio_button">Add to portfolio</string>
<string name="markets_common_my_portfolio">My portfolio</string>
<string name="markets_common_title">Market</string>
<string name="markets_select_wallet">Select wallet</string>
<string name="markets_sort_by_title">Sort By</string>
<string name="onboarding_access_code_feature_1_description">You have to set up a single access code to protect all your cards</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
<string name="onboarding_access_code_feature_2_description">You can set up an individual access code on each card later</string>
@ -528,9 +548,31 @@
<string name="send_validation_invalid_address">Invalid address</string>
<string name="send_wallet_balance_format">%1$s (%2$s)</string>
<string name="sent_transaction_sent_title">Transaction sent</string>
<string name="settings_card_settings_footer">Prepare to scan card you want to setup.</string>
<string name="settings_forget_wallet">Forget wallet</string>
<string name="settings_forget_wallet_footer">This will remove the wallet from the application. The wallet itself can be added again.</string>
<string name="settings_wallet_name_title">Name</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_available">Available</string>
<string name="staking_details_average_reward_rate">Average Reward Rate</string>
<string name="staking_details_estimated_profit">%s est. profit</string>
<string name="staking_details_market_rating">Market rating</string>
<string name="staking_details_metrics_block_header">Metrics</string>
<string name="staking_details_minimum_requirement">Minimum Requirement</string>
<string name="staking_details_no_rewards_to_claim">No rewards to claim</string>
<string name="staking_details_on_stake">On stake</string>
<string name="staking_details_reward_claiming">Reward claiming</string>
<string name="staking_details_reward_schedule">Reward schedule</string>
<string name="staking_details_rewards_to_claim">Rewards to claim: %s</string>
<string name="staking_details_title">Staking %s</string>
<string name="staking_details_unbonding_period">Unbonding Period</string>
<string name="staking_details_warmup_period">Warmup period</string>
<string name="staking_native">Native staking</string>
<string name="staking_notification_earn_rewards_text" formatted="false">Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days.</string>
<string name="staking_notification_earn_rewards_title">Earn staking rewards</string>
<string name="staking_rewards">Rewards</string>
<string name="staking_validator">Validator</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
<string name="story_awe_title">Revolutionary Hardware Wallet</string>
<string name="story_backup_description">Up to 3 physical cards to one wallet</string>
@ -575,6 +617,7 @@
<string name="token_button_unavailability_reason_pending_transaction_sell">Selling funds will be available once the pending transaction(s) in network %s is complete</string>
<string name="token_button_unavailability_reason_pending_transaction_send">Sending funds will be available once the pending transaction(s) in network %s is complete</string>
<string name="token_button_unavailability_reason_sell_unavailable">Selling %s is not available at the moment. Please check our updates.</string>
<string name="token_button_unavailability_reason_staking_unavailable">Staking %s is not available at the moment. Please check our updates.</string>
<string name="token_details_generate_xpub">Generate XPUB</string>
<string name="token_details_hide_alert_hide">Hide</string>
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
@ -609,6 +652,9 @@
<string name="twins_recreate_warning">This action is irreversible. You will not have access to the old wallet.</string>
<string name="twins_scan_twin_with_number">Tap the twin card with number %s and do not remove until the end of the operation</string>
<string name="unlock_wallet_description_full">Use %s or scan a card to have an access to your wallet</string>
<string name="user_push_notification_agreement_argument_one">Stay up to date with the latest features and news</string>
<string name="user_push_notification_agreement_argument_two">Be able to get important market notifications</string>
<string name="user_push_notification_agreement_header">Would you like to usePush-notifications?</string>
<string name="user_wallet_list_add_button">Add new wallet</string>
<string name="user_wallet_list_delete_prompt">Are you sure you want to delete this wallet?</string>
<string name="user_wallet_list_error_unable_to_unlock">An error has occurred, please scan your card to log in</string>

View file

@ -9,9 +9,6 @@ android {
}
dependencies {
/** Project - Common */
implementation(projects.common)
/** Project - Domain */
implementation(projects.domain.tokens.models)
implementation(projects.domain.appTheme.models)
@ -25,6 +22,12 @@ dependencies {
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.paging.runtime)
implementation(deps.androidx.palette)
implementation(deps.androidx.windowManager) {
exclude(
deps.kotlin.coroutines.android.get().module.group,
deps.kotlin.coroutines.android.get().module.name
)
}
/** Compose */
implementation(deps.compose.constraintLayout)
@ -40,11 +43,12 @@ dependencies {
/** Other libraries */
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.permission)
implementation(deps.material)
implementation(deps.compose.shimmer)
implementation(deps.kotlin.immutable.collections)
implementation(deps.zxing.qrCore)
implementation(deps.jodatime)
api(deps.jodatime)
implementation(deps.timber)
implementation(deps.markdown)
}

View file

@ -1,11 +1,19 @@
package com.tangem.core.ui
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Stable
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.message.EventMessageHandler
import com.tangem.core.ui.theme.AppThemeModeHolder
@Stable
interface UiDependencies {
val hapticManager: HapticManager
val appThemeModeHolder: AppThemeModeHolder
val globalSnackbarHostState: SnackbarHostState
val eventMessageHandler: EventMessageHandler
}

View file

@ -0,0 +1,33 @@
package com.tangem.core.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import com.tangem.core.ui.res.TangemTheme
/**
* A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating
* elements and floating button at the bottom of the screen.
*/
@Composable
fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
Box(
modifier = modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size100 + bottomBarHeight)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
backgroundColor,
),
),
),
)
}

View file

@ -0,0 +1,41 @@
package com.tangem.core.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
@Composable
fun BoxWithGradient(
modifier: Modifier = Modifier,
gradient: Brush = BottomGradient,
content: @Composable BoxScope.() -> Unit,
) {
val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current)
Box(modifier = modifier.fillMaxSize()) {
content()
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(TangemTheme.dimens.size164 + bottomInsetsPx.dp)
.background(gradient),
)
}
}
private val BottomGradient: Brush = Brush.verticalGradient(
colors = listOf(
TangemColorPalette.Black.copy(alpha = 0f),
TangemColorPalette.Black.copy(alpha = 0.75f),
TangemColorPalette.Black.copy(alpha = 0.95f),
TangemColorPalette.Black,
),
)

View file

@ -86,6 +86,7 @@ fun PrimaryButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
size: TangemButtonSize = TangemButtonSize.Default,
colors: ButtonColors = TangemButtonsDefaults.primaryButtonColors,
showProgress: Boolean = false,
enabled: Boolean = true,
) {
@ -94,7 +95,7 @@ fun PrimaryButton(
text = text,
icon = TangemButtonIconPosition.None,
onClick = onClick,
colors = TangemButtonsDefaults.primaryButtonColors,
colors = colors,
enabled = enabled,
showProgress = showProgress,
size = size,

View file

@ -27,8 +27,8 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.fields.SimpleDialogTextField
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -100,7 +100,7 @@ fun TextInputDialog(
confirmButton: DialogButton,
onDismissDialog: () -> Unit,
onValueChange: (TextFieldValue) -> Unit,
textFieldParams: AdditionalTextInputDialogParams,
textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() },
title: String? = null,
dismissButton: DialogButton? = null,
isDismissable: Boolean = true,
@ -128,7 +128,7 @@ fun TextInputDialog(
confirmButton: DialogButton,
onDismissDialog: () -> Unit,
onValueChange: (String) -> Unit,
textFieldParams: AdditionalTextInputDialogParams,
textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() },
title: String? = null,
dismissButton: DialogButton? = null,
isDismissable: Boolean = true,

View file

@ -1,12 +0,0 @@
package com.tangem.core.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import com.google.accompanist.systemuicontroller.SystemUiController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@Composable
fun SystemBarsEffect(block: SystemUiController.() -> Unit) {
val systemUiController = rememberSystemUiController()
SideEffect { block(systemUiController) }
}

View file

@ -0,0 +1,61 @@
package com.tangem.core.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.res.LocalIsInDarkTheme
/**
* Provides the ability to set a scrim for 3-button navigation
*
* Automatically makes navigation bar transparent when the composable is disposed.
*
* Usage example: [com.tangem.feature.tokendetails.presentation.TokenDetailsFragment]
*/
@Composable
fun NavigationBar3ButtonsScrim() {
val systemUiController = rememberSystemUiController()
val isDarkTheme = LocalIsInDarkTheme.current
SideEffect {
systemUiController.isNavigationBarContrastEnforced = true
}
LaunchedEffect(systemUiController.isNavigationBarContrastEnforced) {
if (systemUiController.isNavigationBarContrastEnforced.not()) {
systemUiController.isNavigationBarContrastEnforced = true
}
}
DisposableEffect(isDarkTheme) {
onDispose {
systemUiController.isNavigationBarContrastEnforced = false
}
}
}
/**
* Provides the ability to set dark/light icons in cases where the darkness of the screen differs from that
* provided in [TangemTheme].
*
* Automatically returns the icon colors to their original state when the composable is disposed.
*
* Usage example: [com.tangem.feature.qrscanning.QrScanningFragment]
*/
@Composable
fun SystemBarsIconsDisposable(darkIcons: Boolean, isNavigationBarContrastEnforced: Boolean = false) {
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.systemBarsDarkContentEnabled = darkIcons
systemUiController.isNavigationBarContrastEnforced = isNavigationBarContrastEnforced
}
val isDarkTheme = LocalIsInDarkTheme.current
DisposableEffect(isDarkTheme) {
onDispose {
systemUiController.systemBarsDarkContentEnabled = !isDarkTheme
systemUiController.isNavigationBarContrastEnforced = false
}
}
}

View file

@ -1,23 +1,57 @@
package com.tangem.core.ui.components.appbar
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.appbar.models.AdditionalButton
import com.tangem.core.ui.res.TangemThemePreview
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
/**
* App bar with title and two additional buttons
*
* @param startButton button information attached to the left edge
* @param endButton button information attached to the right edge
*
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=50%3A403&t=tRQ4KlzkjV7TCLZl-4">
* Figma Component</a>
*/
@Composable
fun AppBarWithAdditionalButtons(
text: TextReference,
modifier: Modifier = Modifier,
startButton: AdditionalButton? = null,
endButton: AdditionalButton? = null,
textColor: Color = TangemTheme.colors.text.primary1,
iconColor: Color = TangemTheme.colors.icon.primary1,
) {
AppBarWithAdditionalButtons(
text = text.resolveReference(),
startButton = startButton,
endButton = endButton,
modifier = modifier,
textColor = textColor,
iconColor = iconColor,
)
}
/**
* App bar with title and two additional buttons
@ -31,43 +65,63 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
fun AppBarWithAdditionalButtons(
text: String,
modifier: Modifier = Modifier,
startButton: AdditionalButton? = null,
endButton: AdditionalButton? = null,
textColor: Color = TangemTheme.colors.text.primary1,
iconColor: Color = TangemTheme.colors.icon.primary1,
) {
Box(
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.heightIn(TangemTheme.dimens.size56)
.padding(all = TangemTheme.dimens.spacing16),
) {
if (startButton != null) {
IconButton(modifier = Modifier.align(Alignment.CenterStart), onClick = startButton.onIconClicked) {
Icon(
painter = painterResource(id = startButton.iconRes),
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens.size24),
tint = TangemTheme.colors.icon.primary1,
)
}
Icon(
painter = painterResource(id = startButton.iconRes),
contentDescription = null,
tint = iconColor,
modifier = Modifier
.size(TangemTheme.dimens.size24)
.align(Alignment.CenterStart)
.clickable(
onClick = startButton.onIconClicked,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = androidx.compose.material.ripple.rememberRipple(
bounded = false,
radius = TangemTheme.dimens.size24 / 2,
),
),
)
}
Text(
text = text,
modifier = Modifier.align(Alignment.Center),
color = TangemTheme.colors.text.primary1,
color = textColor,
maxLines = 1,
style = TangemTheme.typography.subtitle1,
)
if (endButton != null) {
IconButton(modifier = Modifier.align(Alignment.CenterEnd), onClick = endButton.onIconClicked) {
Icon(
painter = painterResource(id = endButton.iconRes),
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens.size24),
tint = TangemTheme.colors.icon.primary1,
)
}
Icon(
painter = painterResource(id = endButton.iconRes),
contentDescription = null,
tint = iconColor,
modifier = Modifier
.size(TangemTheme.dimens.size24)
.align(Alignment.CenterEnd)
.clickable(
onClick = endButton.onIconClicked,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = androidx.compose.material.ripple.rememberRipple(
bounded = false,
radius = TangemTheme.dimens.size24 / 2,
),
),
)
}
}
}
@ -87,6 +141,7 @@ private fun Preview_AppBarWithAdditionalButtons() {
iconRes = R.drawable.ic_more_vertical_24,
onIconClicked = {},
),
modifier = Modifier.background(TangemTheme.colors.background.secondary),
)
}
}
@ -102,6 +157,7 @@ private fun Preview_AppBarWithOnlyStartButtons() {
iconRes = R.drawable.ic_scan_24,
onIconClicked = {},
),
modifier = Modifier.background(TangemTheme.colors.background.secondary),
)
}
}
@ -117,6 +173,7 @@ private fun Preview_AppBarWithOnlyEndButtons() {
iconRes = R.drawable.ic_more_vertical_24,
onIconClicked = {},
),
modifier = Modifier.background(TangemTheme.colors.background.secondary),
)
}
}

View file

@ -0,0 +1,75 @@
package com.tangem.core.ui.components.appbar.models
import androidx.compose.foundation.layout.size
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TopAppBarMedium(
title: TextReference,
scrollBehavior: TopAppBarScrollBehavior,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
navigationIconResId: Int = R.drawable.ic_back_24,
colors: TopAppBarColors = TangemTopAppBarColors,
) {
MediumTopAppBar(
modifier = modifier,
scrollBehavior = scrollBehavior,
colors = colors,
title = {
val collapsedStyle = TangemTheme.typography.subtitle1
val expandedStyle = TangemTheme.typography.h1
val style by remember(scrollBehavior.state.collapsedFraction) {
derivedStateOf {
if (scrollBehavior.state.collapsedFraction >= COLLAPSED_APP_BAR_THRESHOLD) {
collapsedStyle
} else {
expandedStyle
}
}
}
Text(
text = title.resolveReference(),
style = style,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
navigationIcon = {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = onBackClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = navigationIconResId),
contentDescription = null,
)
}
},
)
}
@OptIn(ExperimentalMaterial3Api::class)
internal val TangemTopAppBarColors: TopAppBarColors
@Composable
@ReadOnlyComposable
get() = TopAppBarColors(
containerColor = TangemTheme.colors.background.secondary,
scrolledContainerColor = TangemTheme.colors.background.secondary,
navigationIconContentColor = TangemTheme.colors.icon.primary1,
titleContentColor = TangemTheme.colors.text.primary1,
actionIconContentColor = TangemTheme.colors.icon.primary1,
)

View file

@ -0,0 +1,85 @@
package com.tangem.core.ui.components.atoms.radiobutton
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* [Radio button](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=101-119&t=FH84ljLBk1vmGAei-4)
*
* @param isSelected Whether the radio button is selected
* @param onClick Called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param isEnabled Whether the button click is enabled
*/
@Composable
fun TangemRadioButton(
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
) {
Box(
modifier = modifier.clickable(
enabled = isEnabled,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.size16),
onClick = onClick,
),
) {
val color = TangemTheme.colors.stroke.secondary
val radius = with(LocalDensity.current) { TangemTheme.dimens.size9.toPx() }
val width = with(LocalDensity.current) { TangemTheme.dimens.size2.toPx() }
Canvas(
modifier = Modifier
.size(TangemTheme.dimens.size24)
.padding(TangemTheme.dimens.spacing2),
) {
drawCircle(
color = color,
radius = radius,
style = Stroke(width),
)
}
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,
)
}
}
}
// region Preview
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemRadioButton_Preview() {
var isSelected by remember { mutableStateOf(false) }
TangemThemePreview {
TangemRadioButton(isSelected, onClick = { isSelected = !isSelected })
}
}
// endregion

View file

@ -0,0 +1,37 @@
package com.tangem.core.ui.components.block
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
@Composable
fun BlockCard(
modifier: Modifier = Modifier,
enabled: Boolean = true,
colors: CardColors = TangemBlockCardColors,
onClick: () -> Unit = {},
content: @Composable ColumnScope.() -> Unit = {},
) {
Card(
modifier = modifier,
onClick = onClick,
shape = TangemTheme.shapes.roundedCornersXMedium,
colors = colors,
enabled = enabled,
content = content,
)
}
val TangemBlockCardColors: CardColors
@Composable
@ReadOnlyComposable
get() = CardColors(
containerColor = TangemTheme.colors.background.primary,
contentColor = TangemTheme.colors.text.primary1,
disabledContainerColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)

View file

@ -0,0 +1,53 @@
package com.tangem.core.ui.components.block
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.text.style.TextOverflow
import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
onClick = model.onClick,
) {
Row(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12, Alignment.Start),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = model.iconRes),
tint = when (model.accentType) {
BlockUM.AccentType.NONE -> TangemTheme.colors.icon.secondary
BlockUM.AccentType.ACCENT -> TangemTheme.colors.text.accent
BlockUM.AccentType.WARNING -> TangemTheme.colors.text.warning
},
contentDescription = null,
)
Text(
text = model.text.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = when (model.accentType) {
BlockUM.AccentType.NONE -> TangemTheme.colors.text.primary1
BlockUM.AccentType.ACCENT -> TangemTheme.colors.text.accent
BlockUM.AccentType.WARNING -> TangemTheme.colors.text.warning
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.core.ui.components.block.model
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
data class BlockUM(
val text: TextReference,
@DrawableRes val iconRes: Int,
val onClick: () -> Unit,
val accentType: AccentType = AccentType.NONE,
) {
enum class AccentType {
NONE, ACCENT, WARNING,
}
}

View file

@ -1,13 +1,14 @@
package com.tangem.core.ui.components.bottomsheets
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
@ -15,6 +16,7 @@ import kotlinx.coroutines.launch
* Tangem bottom sheet with custom draggable header and config
*
* @param config data model containing logic and ui models
* @param
* @param content custom bottom sheet content
*/
@OptIn(ExperimentalMaterial3Api::class)
@ -22,20 +24,33 @@ import kotlinx.coroutines.launch
inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color = TangemTheme.colors.background.primary,
addBottomInsets: Boolean = true,
crossinline content: @Composable ColumnScope.(T) -> Unit,
) {
var isVisible by remember { mutableStateOf(value = config.isShow) }
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
if (isVisible && config.content is T) {
ModalBottomSheet(
// FIXME temporary solution to fix height of the bottom sheet
modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
windowInsets = WindowInsetsZero,
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
) {
content(config.content)
if (addBottomInsets) {
Column(
// FIXME temporary solution to fix height of the bottom sheet
modifier = Modifier.navigationBarsPadding(),
) {
content(config.content)
}
} else {
content(config.content)
}
}
}

View file

@ -8,10 +8,7 @@ import androidx.compose.foundation.LocalIndication
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.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@ -21,8 +18,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
@ -42,7 +40,7 @@ import kotlinx.collections.immutable.persistentListOf
*/
@Composable
inline fun <reified T> SegmentedButtons(
config: PersistentList<T>,
config: ImmutableList<T>,
crossinline onClick: (T) -> Unit,
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.background.tertiary,
@ -51,7 +49,7 @@ inline fun <reified T> SegmentedButtons(
showIndication: Boolean = true,
initialSelectedItem: T? = null,
isEnabled: Boolean = true,
crossinline buttonContent: @Composable (T) -> Unit,
crossinline buttonContent: @Composable BoxScope.(T) -> Unit,
) {
if (config.isEmpty() || config.size == 1) return
@ -97,7 +95,7 @@ inline fun <reified T> SegmentedButtons(
onClick(config[index])
},
) {
buttonContent.invoke(config[index])
buttonContent.invoke(this, config[index])
}
}
}

View file

@ -5,7 +5,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
@ -14,18 +13,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
@ -63,7 +59,7 @@ fun InputRowBestRate(
modifier = Modifier
.padding(TangemTheme.dimens.spacing12),
) {
InnerIcon(imageUrl = imageUrl)
InputRowAsyncImage(imageUrl = imageUrl, modifier = Modifier.size(TangemTheme.dimens.spacing40))
Column(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12),
@ -131,29 +127,6 @@ private fun InnerTitle(title: TextReference, titleExtra: TextReference, showTag:
}
}
@Composable
private fun InnerIcon(imageUrl: String) {
SubcomposeAsyncImage(
modifier = Modifier.size(TangemTheme.dimens.size40),
model = ImageRequest.Builder(context = LocalContext.current)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = { LoadingIcon() },
error = {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.tertiary,
shape = CircleShape,
),
)
},
contentDescription = null,
)
}
//region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -0,0 +1,54 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage
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
@Composable
internal fun InputRowImageBase(
subtitle: TextReference,
caption: TextReference,
imageUrl: String,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
extraContent: @Composable RowScope.() -> Unit = {},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
InputRowAsyncImage(
imageUrl = imageUrl,
modifier = Modifier
.size(TangemTheme.dimens.spacing36)
.padding(vertical = TangemTheme.dimens.size1),
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = TangemTheme.dimens.spacing12),
) {
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = subtitleColor,
)
Text(
text = caption.resolveAnnotatedReference(),
style = TangemTheme.typography.caption2,
color = captionColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
}
extraContent()
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
/**
* Input row component with selector
* [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2100-842&t=hoBXmDX8NeLrp4p6-4)
*
* @param subtitle subtitle text
* @param caption caption text
* @param imageUrl icon to load
* @param modifier modifier
* @param subtitleColor subtitle text color
* @param captionColor caption text color
*/
@Composable
fun InputRowImageChevron(
subtitle: TextReference,
caption: TextReference,
imageUrl: String,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
) {
InputRowImageBase(
subtitle = subtitle,
caption = caption,
imageUrl = imageUrl,
modifier = modifier,
subtitleColor = subtitleColor,
captionColor = captionColor,
) {
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}

View file

@ -0,0 +1,131 @@
package com.tangem.core.ui.components.inputrow
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.atoms.radiobutton.TangemRadioButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.annotatedReference
import com.tangem.core.ui.extensions.combinedReference
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.utils.BigDecimalFormatter
import java.math.BigDecimal
/**
* Input row component with selector
* [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2772-905&t=FH84ljLBk1vmGAei-4)
*
* @param subtitle subtitle text
* @param caption caption text
* @param imageUrl icon to load
* @param onSelect callback when selected
* @param modifier modifier
* @param subtitleColor subtitle text color
* @param captionColor caption text color
* @param isSelected true if selected
*/
@Composable
fun InputRowImageSelector(
subtitle: TextReference,
caption: TextReference,
imageUrl: String,
onSelect: () -> Unit,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
isSelected: Boolean = false,
) {
InputRowImageBase(
subtitle = subtitle,
caption = caption,
imageUrl = imageUrl,
subtitleColor = subtitleColor,
captionColor = captionColor,
modifier = modifier
.clickable(
onClick = onSelect,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
)
.padding(TangemTheme.dimens.spacing12),
) {
TangemRadioButton(isSelected = isSelected, isEnabled = false, onClick = onSelect)
}
}
//region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun InputRowImageSelectorPreview(
@PreviewParameter(InputRowImageSelectorPreviewDataProvider::class) data: InputRowImageSelectorPreviewData,
) {
TangemThemePreview {
InputRowImageSelector(
modifier = Modifier.background(TangemTheme.colors.background.action),
subtitle = data.subtitle,
caption = combinedReference(
resourceReference(R.string.staking_details_apr),
annotatedReference(
buildAnnotatedString {
append(" ")
withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) {
append(
BigDecimalFormatter.formatPercent(BigDecimal.ZERO, true),
)
}
},
),
),
imageUrl = "",
isSelected = false,
onSelect = {},
)
}
}
private data class InputRowImageSelectorPreviewData(
val subtitle: TextReference,
val caption: TextReference,
val showDivider: Boolean,
val actionIconRes: Int?,
val isSelected: Boolean,
)
private class InputRowImageSelectorPreviewDataProvider :
PreviewParameterProvider<InputRowImageSelectorPreviewData> {
override val values: Sequence<InputRowImageSelectorPreviewData>
get() = sequenceOf(
InputRowImageSelectorPreviewData(
subtitle = TextReference.Str("subtitle"),
caption = TextReference.Str("caption"),
actionIconRes = null,
showDivider = false,
isSelected = false,
),
InputRowImageSelectorPreviewData(
subtitle = TextReference.Str("subtitle"),
caption = TextReference.Str("caption"),
actionIconRes = R.drawable.ic_chevron_right_24,
showDivider = true,
isSelected = true,
),
)
}
//endregion

View file

@ -0,0 +1,41 @@
package com.tangem.core.ui.components.inputrow.inner
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
import com.tangem.core.ui.res.TangemTheme
/**
* Loads image by url for icon in the input row
*
* @param imageUrl url of the image
* @param modifier modifier
*/
@Composable
internal fun InputRowAsyncImage(imageUrl: String, modifier: Modifier = Modifier) {
SubcomposeAsyncImage(
modifier = modifier,
model = ImageRequest.Builder(context = LocalContext.current)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = { LoadingIcon() },
error = {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.tertiary,
shape = CircleShape,
),
)
},
contentDescription = null,
)
}

View file

@ -36,6 +36,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemColorPalette.White
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Travala notification with image background
@ -192,7 +193,7 @@ private fun formatSubtitle(subtitle: String): AnnotatedString {
@Preview
@Composable
private fun TravalaNotificationWithBackgroundPreview() {
TangemTheme {
TangemThemePreview {
TravalaNotificationWithBackground(
config = NotificationConfig(
title = resourceReference(

View file

@ -0,0 +1,147 @@
package com.tangem.core.ui.components.rows
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
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.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.R
@Suppress("LongParameterList")
@Composable
fun RoundableCornersRow(
startText: String,
startTextColor: Color,
startTextStyle: TextStyle,
endText: String,
endTextColor: Color,
endTextStyle: TextStyle,
cornersToRound: CornersToRound,
iconResId: Int? = null,
) {
Surface(
shape = cornersToRound.getShape(),
color = TangemTheme.colors.background.primary,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(TangemTheme.dimens.size48)
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = startText,
color = startTextColor,
maxLines = 1,
style = startTextStyle,
)
if (iconResId != null) {
Icon(
modifier = Modifier
.padding(TangemTheme.dimens.spacing4)
.size(TangemTheme.dimens.size16),
painter = painterResource(id = R.drawable.ic_alert_24),
contentDescription = null,
tint = TangemTheme.colors.text.tertiary,
)
}
Spacer(modifier = Modifier.weight(1f))
Text(
text = endText,
color = endTextColor,
maxLines = 1,
style = endTextStyle,
)
}
}
}
enum class CornersToRound {
ALL_4,
TOP_2,
BOTTOM_2,
ZERO,
;
@Suppress("TopLevelComposableFunctions")
@Composable
fun getShape(): RoundedCornerShape {
val radius = TangemTheme.dimens.radius12
return when (this) {
ALL_4 -> RoundedCornerShape(radius)
TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius)
BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius)
ZERO -> RoundedCornerShape(0.dp)
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_RoundableCornersRow(
@PreviewParameter(RoundableCornersRowDataProvider::class) previewData: RoundableCornersRowPreviewData,
) {
TangemThemePreview {
Box(modifier = Modifier.background(color = TangemTheme.colors.icon.attention)) {
RoundableCornersRow(
startText = previewData.startText,
startTextColor = TangemTheme.colors.text.tertiary,
startTextStyle = TangemTheme.typography.subtitle2,
endText = previewData.endText,
endTextColor = TangemTheme.colors.text.primary1,
endTextStyle = TangemTheme.typography.subtitle2,
cornersToRound = previewData.cornersToRound,
iconResId = previewData.iconResId,
)
}
}
}
private data class RoundableCornersRowPreviewData(
val startText: String,
val endText: String,
val cornersToRound: CornersToRound,
val iconResId: Int? = null,
)
private class RoundableCornersRowDataProvider :
PreviewParameterProvider<RoundableCornersRowPreviewData> {
override val values: Sequence<RoundableCornersRowPreviewData>
get() = sequenceOf(
getPreviewData(cornersToRound = CornersToRound.ZERO),
getPreviewData(cornersToRound = CornersToRound.TOP_2),
getPreviewData(cornersToRound = CornersToRound.BOTTOM_2),
getPreviewData(cornersToRound = CornersToRound.ZERO, iconResId = R.drawable.ic_alert_24),
getPreviewData(cornersToRound = CornersToRound.TOP_2, iconResId = R.drawable.ic_alert_24),
getPreviewData(cornersToRound = CornersToRound.BOTTOM_2, iconResId = R.drawable.ic_alert_24),
)
private fun getPreviewData(cornersToRound: CornersToRound, iconResId: Int? = null) = RoundableCornersRowPreviewData(
startText = "startText",
endText = "endText",
cornersToRound = cornersToRound,
iconResId = iconResId,
)
}

View file

@ -25,6 +25,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.utils.Strings
@Composable
fun SelectorRowItem(
@ -127,7 +128,7 @@ private fun RowScope.SelectorValueContent(
)
if (postDot != null) {
Text(
text = "",
text = Strings.DOT,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,

View file

@ -0,0 +1,143 @@
package com.tangem.core.ui.components.showcase
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.showcase.model.ShowcaseButtonModel
import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel
import com.tangem.core.ui.extensions.TextReference
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* @param headerIconRes big header icon
* @param headerText header text
* @param showcaseItems list of bullet points
* @param primaryButton primary button
* @param secondaryButton secondary button
* @param modifier compose modifier
*
* @see <a href = "https://www.figma.com/design/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=12620-90568&t=WeCOVTgeYfkr24Ff-4"
* >Figma component</a>
*/
@Composable
fun Showcase(
@DrawableRes headerIconRes: Int,
headerText: TextReference,
showcaseItems: ImmutableList<ShowcaseItemModel>,
primaryButton: ShowcaseButtonModel,
secondaryButton: ShowcaseButtonModel,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxSize(),
) {
ShowcaseContent(
headerIconRes = headerIconRes,
headerText = headerText,
showcaseItems = showcaseItems,
modifier = Modifier
.weight(1f)
.align(Alignment.CenterHorizontally),
)
ShowcaseButtons(
primaryButtonText = primaryButton.buttonText,
onPrimaryClick = primaryButton.onClick,
secondaryButtonText = secondaryButton.buttonText,
onSecondaryClick = secondaryButton.onClick,
)
}
}
@Composable
private fun ShowcaseButtons(
primaryButtonText: TextReference,
secondaryButtonText: TextReference,
onPrimaryClick: () -> Unit,
onSecondaryClick: () -> Unit,
hint: TextReference? = null,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
PrimaryButton(
text = primaryButtonText.resolveReference(),
onClick = onPrimaryClick,
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
),
)
SecondaryButton(
text = secondaryButtonText.resolveReference(),
onClick = onSecondaryClick,
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing16,
),
)
hint?.let {
Text(
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing12,
),
)
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360, heightDp = 720)
@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Showcase_Preview() {
TangemThemePreview {
Showcase(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
R.drawable.ic_rocket_launch_24,
resourceReference(R.string.user_push_notification_agreement_argument_one),
),
ShowcaseItemModel(
R.drawable.ic_storefront_24,
resourceReference(R.string.user_push_notification_agreement_argument_two),
),
),
primaryButton = ShowcaseButtonModel(resourceReference(R.string.common_allow), {}),
secondaryButton = ShowcaseButtonModel(resourceReference(R.string.common_later), {}),
modifier = Modifier.background(TangemTheme.colors.background.primary),
)
}
}
// endregion

View file

@ -0,0 +1,71 @@
package com.tangem.core.ui.components.showcase
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.text.style.TextAlign
import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import kotlinx.collections.immutable.ImmutableList
@Composable
fun ShowcaseContent(
@DrawableRes headerIconRes: Int,
headerText: TextReference,
showcaseItems: ImmutableList<ShowcaseItemModel>,
modifier: Modifier = Modifier,
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier,
) {
Icon(
painter = painterResource(id = headerIconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.size(TangemTheme.dimens.size56),
)
Text(
text = headerText.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(
start = TangemTheme.dimens.spacing34,
end = TangemTheme.dimens.spacing34,
top = TangemTheme.dimens.spacing28,
),
)
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing34,
end = TangemTheme.dimens.spacing34,
top = TangemTheme.dimens.spacing28,
),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
horizontalAlignment = Alignment.CenterHorizontally,
) {
repeat(showcaseItems.size) { index ->
ShowcaseItem(
iconRes = showcaseItems[index].iconRes,
text = showcaseItems[index].text,
)
}
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.core.ui.components.showcase
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun ShowcaseItem(@DrawableRes iconRes: Int, text: TextReference) {
Row {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
Text(
text = text.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing20),
)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.ui.components.showcase.model
import com.tangem.core.ui.extensions.TextReference
data class ShowcaseButtonModel(
val buttonText: TextReference,
val onClick: () -> Unit,
)

View file

@ -0,0 +1,9 @@
package com.tangem.core.ui.components.showcase.model
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
data class ShowcaseItemModel(
@DrawableRes val iconRes: Int,
val text: TextReference,
)

View file

@ -19,6 +19,7 @@ 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.TangemThemePreview
/**
* Snackbar to inform the user about copying text to the clipboard
@ -100,7 +101,7 @@ private fun MessageText(text: TextReference, modifier: Modifier = Modifier) {
private fun Preview_CopiedTextSnackbar(
@PreviewParameter(CopiedTextSnackbarDataProvider::class) message: TextReference,
) {
TangemTheme(isDark = false) {
TangemThemePreview {
CopiedTextSnackbar(message = message)
}
}

View file

@ -22,7 +22,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.tangem.common.Strings
import com.tangem.core.ui.R
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
@ -35,6 +34,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.utils.Strings
import java.util.UUID
/**

View file

@ -0,0 +1,13 @@
package com.tangem.core.ui.decompose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
@Stable
fun interface ComposableContentComponent {
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
fun Content(modifier: Modifier)
}

View file

@ -0,0 +1,14 @@
package com.tangem.core.ui.decompose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
@Stable
interface ComposableDialogComponent {
val doOnDismiss: () -> Unit
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
fun Dialog()
}

View file

@ -11,6 +11,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import org.intellij.markdown.MarkdownElementTypes
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
/**
* Utility class for creating text as [String] or [StringRes].
@ -46,6 +48,13 @@ sealed interface TextReference {
*/
data class Str(val value: String) : TextReference
/**
* Annotated text
*
* @property value annotated string
*/
data class Annotated(val value: AnnotatedString) : TextReference
/**
* Combined reference. It concatenates all [refs].
*
@ -81,6 +90,16 @@ fun stringReference(value: String): TextReference {
return TextReference.Str(value)
}
/**
* Creates a [TextReference] using an annotated string value.
*
* @param value The annotated string value.
* @return A [TextReference] representing the provided annotated string value.
*/
fun annotatedReference(value: AnnotatedString): TextReference {
return TextReference.Annotated(value)
}
/**
* Creates a [TextReference] using a plural string resource ID with count and optional format arguments.
*
@ -131,6 +150,7 @@ fun TextReference.resolveReference(): String {
}
is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Annotated -> value.text
is TextReference.Combined -> {
buildString {
refs.forEach {
@ -153,6 +173,7 @@ fun TextReference.resolveReference(resources: Resources): String {
}
is TextReference.PluralRes -> resources.getQuantityString(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Annotated -> value.text
is TextReference.Combined -> {
buildString {
refs.forEach {
@ -178,9 +199,10 @@ fun TextReference.resolveAnnotatedReference(): AnnotatedString {
pluralStringResource(id, count, *formatArgs.toTypedArray()),
)
is TextReference.Str -> formatAnnotated(value)
is TextReference.Annotated -> value
is TextReference.Combined -> buildAnnotatedString {
refs.forEach {
append(formatAnnotated(it.resolveReference()))
append(it.resolveAnnotatedReference())
}
}
}
@ -193,10 +215,21 @@ operator fun TextReference.plus(ref: TextReference): TextReference {
is TextReference.PluralRes,
is TextReference.Res,
is TextReference.Str,
is TextReference.Annotated,
-> TextReference.Combined(refs = wrappedList(this, ref))
}
}
@Suppress("NOTHING_TO_INLINE")
@OptIn(ExperimentalContracts::class)
inline fun TextReference?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}
return this == null || this == TextReference.EMPTY
}
@Composable
private fun formatAnnotated(rawString: String): AnnotatedString {
val markdownDescriptor = rememberMarkdownParser()

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.message
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.ui.UiMessage
import com.tangem.core.decompose.ui.UiMessageHandler
import com.tangem.core.ui.event.StateEvent
@ -11,6 +12,7 @@ import kotlinx.coroutines.flow.StateFlow
/**
* Message handler that is used to show or remove an [EventMessage] in the UI.
*/
@Stable
class EventMessageHandler(
private val events: MutableStateFlow<StateEvent<EventMessage>> = MutableStateFlow(consumedEvent()),
) : UiMessageHandler, StateFlow<StateEvent<EventMessage>> by events {

View file

@ -83,7 +83,6 @@ class TangemColors internal constructor(
warning: Color,
attention: Color,
accent: Color = TangemColorPalette.Azure,
constant: Color = TangemColorPalette.White,
) {
var primary1 by mutableStateOf(primary1)
private set

View file

@ -47,6 +47,7 @@ data class TangemDimens internal constructor(
val size5: Dp = 5.dp,
val size7: Dp = 7.dp,
val size8: Dp = 8.dp,
val size9: Dp = 9.dp,
val size10: Dp = 10.dp,
val size11: Dp = 11.dp,
val size12: Dp = 12.dp,
@ -81,8 +82,10 @@ data class TangemDimens internal constructor(
val size90: Dp = 90.dp,
val size93: Dp = 93.dp,
val size96: Dp = 96.dp,
val size100: Dp = 100.dp,
val size102: Dp = 102.dp,
val size108: Dp = 108.dp,
val size110: Dp = 110.dp,
val size116: Dp = 116.dp,
val size120: Dp = 120.dp,
val size142: Dp = 142.dp,
@ -99,9 +102,11 @@ data class TangemDimens internal constructor(
val spacing2: Dp = 2.dp,
val spacing3: Dp = 3.dp,
val spacing4: Dp = 4.dp,
val spacing5: Dp = 5.dp,
val spacing6: Dp = 6.dp,
val spacing8: Dp = 8.dp,
val spacing10: Dp = 10.dp,
val spacing11: Dp = 11.dp,
val spacing12: Dp = 12.dp,
val spacing14: Dp = 14.dp,
val spacing15: Dp = 15.dp,

View file

@ -3,19 +3,22 @@ package com.tangem.core.ui.res
import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.haptic.MockHapticManager
// TODO: use isSystemInDarkTheme() for automatic color detection
internal const val IS_SYSTEM_IN_DARK_THEME: Boolean = false
import com.tangem.core.ui.windowsize.WindowSize
@Composable
fun TangemTheme(
isDark: Boolean = false,
windowSize: WindowSize,
typography: TangemTypography = TangemTheme.typography,
dimens: TangemDimens = TangemTheme.dimens,
hapticManager: HapticManager = MockHapticManager,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
content: @Composable () -> Unit,
) {
val themeColors = if (isDark) darkThemeColors() else lightThemeColors()
@ -23,6 +26,16 @@ fun TangemTheme(
.also { it.update(themeColors) }
val shapes = remember { TangemShapes(dimens) }
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.setSystemBarsColor(
color = Color.Transparent,
darkIcons = !isDark,
isNavigationBarContrastEnforced = false,
)
}
MaterialTheme(
colors = materialThemeColors(colors = themeColors, isDark = isDark),
) {
@ -33,6 +46,8 @@ fun TangemTheme(
LocalTangemShapes provides shapes,
LocalIsInDarkTheme provides isDark,
LocalHapticManager provides hapticManager,
LocalSnackbarHostState provides snackbarHostState,
LocalWindowSize provides windowSize,
) {
ProvideTextStyle(
value = TangemTheme.typography.body1,
@ -204,4 +219,12 @@ val LocalIsInDarkTheme = staticCompositionLocalOf { false }
val LocalHapticManager = staticCompositionLocalOf<HapticManager> {
error("No HapticManager provided")
}
val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
error("No SnackbarHostState provided")
}
val LocalWindowSize = staticCompositionLocalOf<WindowSize> {
error("No WindowSize provided")
}

View file

@ -1,7 +1,9 @@
package com.tangem.core.ui.res
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.runtime.Composable
import com.tangem.core.ui.windowsize.rememberWindowSizePreview
@Composable
fun TangemThemePreview(
@ -12,10 +14,13 @@ fun TangemThemePreview(
) {
val isDarkTheme = isDark ?: isSystemInDarkTheme()
TangemTheme(
isDark = isDarkTheme,
typography = typography,
dimens = dimens,
content = content,
)
BoxWithConstraints {
TangemTheme(
isDark = isDarkTheme,
typography = typography,
dimens = dimens,
windowSize = rememberWindowSizePreview(maxWidth, maxHeight),
content = content,
)
}
}

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