Updated on 2026-08-14
This commit is contained in:
commit
6437018fc8
1160 changed files with 35466 additions and 6540 deletions
1
core/ab-tests/.gitignore
vendored
Normal file
1
core/ab-tests/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
31
core/ab-tests/build.gradle.kts
Normal file
31
core/ab-tests/build.gradle.kts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
alias(deps.plugins.ksp)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.abtests"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.timber)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Amplitude experiment */
|
||||
implementation(deps.amplitude.experiment)
|
||||
}
|
||||
8
core/ab-tests/detekt-baseline-debug.xml
Normal file
8
core/ab-tests/detekt-baseline-debug.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>NullableToStringCall:AmplitudeABTestsManager.kt$AmplitudeABTestsManager$${variant.key}</ID>
|
||||
<ID>NullableToStringCall:AmplitudeABTestsManager.kt$AmplitudeABTestsManager$${variant.payload}</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.core.abtests.di
|
||||
|
||||
import android.app.Application
|
||||
import com.tangem.core.abtests.BuildConfig
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager
|
||||
import com.tangem.core.abtests.manager.impl.StubABTestsManager
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object ABTestsManagerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideABTestsManager(
|
||||
application: Application,
|
||||
environmentConfigStorage: EnvironmentConfigStorage,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): ABTestsManager {
|
||||
return if (BuildConfig.AB_TESTS_ENABLED) {
|
||||
StubABTestsManager()
|
||||
} else {
|
||||
AmplitudeABTestsManager(
|
||||
application = application,
|
||||
apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().amplitudeApiKey },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.core.abtests.manager
|
||||
|
||||
interface ABTestsManager {
|
||||
|
||||
fun init()
|
||||
|
||||
fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?)
|
||||
|
||||
fun removeUserProperties()
|
||||
|
||||
fun getValue(key: String, defaultValue: String): String
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.core.abtests.manager.impl
|
||||
|
||||
import android.app.Application
|
||||
import com.amplitude.experiment.Experiment
|
||||
import com.amplitude.experiment.ExperimentClient
|
||||
import com.amplitude.experiment.ExperimentConfig
|
||||
import com.amplitude.experiment.ExperimentUser
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class AmplitudeABTestsManager(
|
||||
val application: Application,
|
||||
val apiKeyProvider: Provider<String>,
|
||||
val scope: CoroutineScope,
|
||||
) : ABTestsManager {
|
||||
|
||||
private lateinit var client: ExperimentClient
|
||||
|
||||
override fun init() {
|
||||
if (::client.isInitialized) {
|
||||
Timber.w("AB Tests manager already initialized, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
client = Experiment.initializeWithAmplitudeAnalytics(
|
||||
application = application,
|
||||
apiKey = apiKeyProvider(),
|
||||
config = ExperimentConfig
|
||||
.builder()
|
||||
.automaticFetchOnAmplitudeIdentityChange(true)
|
||||
.build(),
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
client.fetch().get()
|
||||
val allVariants = client.all()
|
||||
logAllVariants(allVariants)
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception, "Failed to fetch AB test variants")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?) {
|
||||
val userProperties = mutableMapOf<String, Any>()
|
||||
batch?.let { userProperties[AnalyticsParam.BATCH] = it }
|
||||
productType?.let { userProperties[AnalyticsParam.PRODUCT_TYPE] = it }
|
||||
firmware?.let { userProperties[AnalyticsParam.FIRMWARE] = it }
|
||||
|
||||
client.setUser(
|
||||
ExperimentUser
|
||||
.builder()
|
||||
.userId(userId)
|
||||
.userProperties(userProperties)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun removeUserProperties() {
|
||||
client.setUser(ExperimentUser())
|
||||
}
|
||||
|
||||
override fun getValue(key: String, defaultValue: String): String {
|
||||
return client.variant(key).value ?: defaultValue
|
||||
}
|
||||
|
||||
private fun logAllVariants(allVariants: Map<String, com.amplitude.experiment.Variant>) {
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
Timber.d("AB Tests: Fetched ${allVariants.size} variants")
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
|
||||
if (allVariants.isEmpty()) {
|
||||
Timber.d("No variants available")
|
||||
} else {
|
||||
allVariants.entries.forEachIndexed { index, (key, variant) ->
|
||||
Timber.d("[${index + 1}/${allVariants.size}] Key: $key")
|
||||
Timber.d(" → Value: ${variant.value ?: "null"}")
|
||||
Timber.d(" → Payload: ${variant.payload}")
|
||||
Timber.d(" → Key: ${variant.key}")
|
||||
Timber.d("-".repeat(SEPARATOR_LENGTH))
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SEPARATOR_LENGTH = 50
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.core.abtests.manager.impl
|
||||
|
||||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
|
||||
internal class StubABTestsManager : ABTestsManager {
|
||||
|
||||
override fun init() {
|
||||
// intentionally do nothing
|
||||
}
|
||||
|
||||
override fun setUserProperties(userId: String?, batch: String?, productType: String?, firmware: String?) {
|
||||
// intentionally do nothing
|
||||
}
|
||||
|
||||
override fun removeUserProperties() {
|
||||
// intentionally do nothing
|
||||
}
|
||||
|
||||
override fun getValue(key: String, defaultValue: String): String {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
|
@ -84,6 +84,7 @@ sealed class AnalyticsParam {
|
|||
data object LongTap : ScreensSources("Long Tap")
|
||||
data object Markets : ScreensSources("Markets")
|
||||
data object HotWallet : ScreensSources("Hot Wallet")
|
||||
data object TangemPay : ScreensSources("Tangem Pay")
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
|
|
|
|||
|
|
@ -61,9 +61,9 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
return paramsInterceptors.remove(interceptorId)
|
||||
}
|
||||
|
||||
override fun setUserId(userWalletId: String) {
|
||||
override fun setUserId(userId: String) {
|
||||
analyticsScope.launch {
|
||||
val userIdHash = userWalletId.hexToBytes()
|
||||
val userIdHash = userId.hexToBytes()
|
||||
.calculateSha256()
|
||||
.toHexString()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,26 @@
|
|||
package com.tangem.core.analytics.utils
|
||||
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface AnalyticsContextProxy {
|
||||
interface TrackingContextProxy {
|
||||
|
||||
fun setContext(scanResponse: ScanResponse)
|
||||
|
||||
fun setContext(userWallet: UserWallet)
|
||||
|
||||
fun addContext(userWallet: UserWallet)
|
||||
|
||||
fun setHotWalletContext()
|
||||
|
||||
fun eraseContext()
|
||||
|
||||
fun addContext(scanResponse: ScanResponse)
|
||||
|
||||
fun addHotWalletContext()
|
||||
|
||||
fun removeContext()
|
||||
}
|
||||
|
|
@ -49,12 +49,8 @@ dependencies {
|
|||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(projects.test.core)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
}
|
||||
|
||||
val generateFeatureToggles by tasks.registering {
|
||||
|
|
@ -93,9 +89,9 @@ fun Task.generateToggles(inputFilePath: String, generatedFileName: String) {
|
|||
.add("mapOf(\n")
|
||||
.indent()
|
||||
.apply {
|
||||
entries.forEachIndexed { index, entry ->
|
||||
entries.forEach { entry ->
|
||||
add(entry)
|
||||
if (index != entries.lastIndex) add(",\n") else add("\n")
|
||||
add(",\n")
|
||||
}
|
||||
}
|
||||
.unindent()
|
||||
|
|
@ -117,5 +113,15 @@ fun Task.generateToggles(inputFilePath: String, generatedFileName: String) {
|
|||
val outputPackageDir = File(outputDir, "")
|
||||
outputPackageDir.mkdirs()
|
||||
fileSpec.writeTo(outputPackageDir)
|
||||
|
||||
// Remove redundant public visibility modifiers
|
||||
val generatedFile = File(outputPackageDir, "com/tangem/core/configtoggle/$generatedFileName.kt")
|
||||
if (generatedFile.exists()) {
|
||||
val content = generatedFile.readText()
|
||||
val fixedContent = content
|
||||
.replace("public object ", "object ")
|
||||
.replace("public val ", "val ")
|
||||
generatedFile.writeText(fixedContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
10
core/config-toggles/detekt-baseline-debug.xml
Normal file
10
core/config-toggles/detekt-baseline-debug.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>DoubleMutabilityForCollection:DevExcludedBlockchainsManager.kt$DevExcludedBlockchainsManager$private var blockchainTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()</ID>
|
||||
<ID>DoubleMutabilityForCollection:DevFeatureTogglesManager.kt$DevFeatureTogglesManager$private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()</ID>
|
||||
<ID>Indentation:ExcludedBlockchainToggles.kt$ExcludedBlockchainToggles$ </ID>
|
||||
<ID>Indentation:FeatureToggles.kt$FeatureToggles$ </ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -30,9 +30,5 @@
|
|||
{
|
||||
"name": "zklink",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "scroll",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
@ -19,6 +19,10 @@
|
|||
"name": "STAKING_CARDANO_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "STAKING_ETH_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "USEDESK_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -50,5 +54,9 @@
|
|||
{
|
||||
"name": "ACCOUNTS_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "FEED_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.manager.ProdFeatureTogglesManagerTest.IsFeatureEnabledModel
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
|
|
|||
|
|
@ -90,10 +90,6 @@ dependencies {
|
|||
implementation(deps.room.ktx)
|
||||
ksp(deps.room.compiler)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(projects.test.core)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
}
|
||||
17
core/datasource/detekt-baseline-debug.xml
Normal file
17
core/datasource/detekt-baseline-debug.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>CastNullableToNonNullableType:ApiResponseCallAdapterFactory.kt$ApiResponseCallAdapterFactory$as</ID>
|
||||
<ID>MultilineLambdaItParameter:DefaultWalletManagersStore.kt$DefaultWalletManagersStore${ it.wallet.blockchain == walletManager.wallet.blockchain && it.wallet.publicKey.derivationPath == walletManager.wallet.publicKey.derivationPath }</ID>
|
||||
<ID>NestedScopeFunctions:PendingActionConverter.kt$PendingActionConverter$let { amount -> PendingAction.PendingActionArgs.Amount( required = amount.required, minimum = amount.minimum, maximum = amount.maximum, ) }</ID>
|
||||
<ID>NestedScopeFunctions:PendingActionConverter.kt$PendingActionConverter$let { duration -> PendingAction.PendingActionArgs.Duration( required = duration.required, minimum = duration.minimum, maximum = duration.maximum, ) }</ID>
|
||||
<ID>NestedScopeFunctions:PendingActionConverter.kt$PendingActionConverter$let { tronResource -> PendingAction.PendingActionArgs.TronResource( required = tronResource.required, options = tronResource.options, ) }</ID>
|
||||
<ID>NestedScopeFunctions:RetrofitApiBuilder.kt$RetrofitApiBuilder$let { withConnectTimeout(timeout = it.duration, unit = it.unit) }</ID>
|
||||
<ID>NestedScopeFunctions:RetrofitApiBuilder.kt$RetrofitApiBuilder$let { withReadTimeout(timeout = it.duration, unit = it.unit) }</ID>
|
||||
<ID>NestedScopeFunctions:RetrofitApiBuilder.kt$RetrofitApiBuilder$let { withWriteTimeout(timeout = it.duration, unit = it.unit) }</ID>
|
||||
<ID>UnreachableCode:MockApiConfigsManager.kt$MockApiConfigsManager$apiConfigs + (apiConfig to environment)</ID>
|
||||
<ID>UnreachableCode:MockApiConfigsManager.kt$MockApiConfigsManager$val apiConfig = apiConfigs.keys.firstOrNull { it.id.name == id } ?: error("Api config with id [$id] not found. Check that ApiConfig with id [$id] was provided into DI")</ID>
|
||||
<ID>UnusedImports:NetworkModule.kt$import com.tangem.datasource.api.common.config.MoonPay</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -24,9 +24,12 @@ sealed class ApiConfig {
|
|||
Express,
|
||||
TangemTech,
|
||||
StakeKit,
|
||||
P2PEthPool,
|
||||
TangemPay,
|
||||
BlockAid,
|
||||
YieldSupply,
|
||||
MoonPay,
|
||||
News,
|
||||
}
|
||||
|
||||
private fun initializeId(): ID {
|
||||
|
|
@ -34,9 +37,12 @@ sealed class ApiConfig {
|
|||
is Express -> ID.Express
|
||||
is TangemTech -> ID.TangemTech
|
||||
is StakeKit -> ID.StakeKit
|
||||
is P2PEthPool -> ID.P2PEthPool
|
||||
is TangemPay -> ID.TangemPay
|
||||
is BlockAid -> ID.BlockAid
|
||||
is YieldSupply -> ID.YieldSupply
|
||||
is MoonPay -> ID.MoonPay
|
||||
is News -> ID.News
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
|
||||
/**
|
||||
* MoonPay [ApiConfig]
|
||||
*/
|
||||
internal class MoonPay : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createProdEnvironment(),
|
||||
createMockEnvironment(),
|
||||
)
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE,
|
||||
-> ApiEnvironment.MOCK
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.moonpay.com/",
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMockEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.utils.RequestHeader
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
/**
|
||||
* News [ApiConfig]
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class News(
|
||||
private val authProvider: AuthProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createProdEnvironment(),
|
||||
createDevEnvironment(),
|
||||
)
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE,
|
||||
DEBUG_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = PROD_BASE_URL,
|
||||
headers = createHeaders(ApiEnvironment.PROD),
|
||||
)
|
||||
|
||||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = DEV_BASE_URL,
|
||||
headers = createHeaders(ApiEnvironment.DEV),
|
||||
)
|
||||
|
||||
private fun createHeaders(environment: ApiEnvironment) = buildMap {
|
||||
putAll(
|
||||
RequestHeader.TangemApiKeyHeader(
|
||||
authProvider = authProvider,
|
||||
apiEnvironment = Provider { environment },
|
||||
).values,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val PROD_BASE_URL = "https://api.tangem.org/"
|
||||
private const val DEV_BASE_URL = "[REDACTED_ENV_URL]"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
|
||||
/**
|
||||
* P2P.org Ethereum Pooled Staking API configuration
|
||||
*/
|
||||
internal class P2PEthPool(
|
||||
private val p2pAuthProvider: P2PEthPoolAuthProvider,
|
||||
) : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
|
||||
|
||||
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
|
||||
createProdEnvironment(),
|
||||
createTestEnvironment(),
|
||||
createMockEnvironment(),
|
||||
)
|
||||
|
||||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||
DEBUG_BUILD_TYPE,
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProdEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.p2p.org/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTestEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "https://api-test.p2p.org/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMockEnvironment(): ApiEnvironmentConfig {
|
||||
return ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.MOCK,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createHeaders() = buildMap {
|
||||
put(key = "Authorization", value = ProviderSuspend { "Bearer ${p2pAuthProvider.getApiKey()}" })
|
||||
put(key = "accept", value = ProviderSuspend { "application/json" })
|
||||
put(key = "Content-Type", value = ProviderSuspend { "application/json" })
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ internal class TangemPay(
|
|||
|
||||
private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
baseUrl = "https://api.dev.us.paera.com/bff/",
|
||||
headers = createHeaders(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,12 @@ sealed class ApiResponse<T : Any> {
|
|||
* Represents a successful response from the API
|
||||
*
|
||||
* @property data the data returned by the API
|
||||
* @property code the HTTP status code of the response
|
||||
* @property headers the headers returned by the API
|
||||
*/
|
||||
data class Success<T : Any>(
|
||||
val data: T,
|
||||
val code: ApiResponseError.HttpException.Code = ApiResponseError.HttpException.Code.OK,
|
||||
override val headers: Map<String, List<String>> = emptyMap(),
|
||||
) : ApiResponse<T>()
|
||||
|
||||
|
|
@ -37,11 +39,20 @@ sealed class ApiResponse<T : Any> {
|
|||
* Wraps data in a [ApiResponse.Success] instance
|
||||
*
|
||||
* @param data the data to wrap
|
||||
* @param code the HTTP status code of the response
|
||||
* @param headers the headers returned by the API
|
||||
* @return a [ApiResponse.Success] instance containing the provided data
|
||||
*/
|
||||
internal fun <T : Any> apiSuccess(data: T, headers: Map<String, List<String>>): ApiResponse<T> {
|
||||
return ApiResponse.Success(data, headers)
|
||||
internal fun <T : Any> apiSuccess(
|
||||
data: T,
|
||||
code: ApiResponseError.HttpException.Code?,
|
||||
headers: Map<String, List<String>>,
|
||||
): ApiResponse<T> {
|
||||
return ApiResponse.Success(
|
||||
data = data,
|
||||
code = code ?: ApiResponseError.HttpException.Code.OK,
|
||||
headers = headers,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -18,8 +18,13 @@ sealed class ApiResponseError : Exception() {
|
|||
val errorBody: String?,
|
||||
) : ApiResponseError() {
|
||||
|
||||
// TODO: extract Code from HttpException
|
||||
// region Error Codes
|
||||
enum class Code(val numericCode: Int) {
|
||||
// 2xx Success
|
||||
OK(numericCode = 200),
|
||||
CREATED(numericCode = 201),
|
||||
ACCEPTED(numericCode = 202),
|
||||
// 3xx Server Errors
|
||||
NOT_MODIFIED(numericCode = 304),
|
||||
// 4xx Server Errors
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: Anal
|
|||
val headers = headers().toMultimap()
|
||||
val body = body()
|
||||
|
||||
val code = ApiResponseError.HttpException.Code.entries
|
||||
.firstOrNull { it.numericCode == code() }
|
||||
|
||||
return if (isSuccessful && body != null) {
|
||||
apiSuccess(data = body, headers = headers)
|
||||
apiSuccess(data = body, code = code, headers = headers)
|
||||
} else {
|
||||
val code = ApiResponseError.HttpException.Code.entries
|
||||
.firstOrNull { it.numericCode == code() }
|
||||
val e = try {
|
||||
if (code == null) {
|
||||
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.datasource.api.ethpool
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolDepositRequest
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolUnstakeRequest
|
||||
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolWithdrawRequest
|
||||
import com.tangem.datasource.api.ethpool.models.response.*
|
||||
import retrofit2.http.*
|
||||
|
||||
/**
|
||||
* P2P.org Ethereum Pooled Staking API client
|
||||
*
|
||||
* Documentation: https://docs.p2p.org/
|
||||
*
|
||||
* Base URL: https://api.p2p.org (prod) / https://api-test.p2p.org (testnet)
|
||||
*/
|
||||
interface P2PEthPoolApi {
|
||||
|
||||
/**
|
||||
* Get list of available vaults
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi" (testnet)
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/vaults")
|
||||
suspend fun getVaults(
|
||||
@Path("network") network: String = "mainnet",
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolVaultsResponse>>
|
||||
|
||||
/**
|
||||
* Prepare deposit transaction
|
||||
*
|
||||
* Create unsigned transaction for depositing ETH into a vault.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Deposit parameters (delegator address, vault address, amount)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/deposit")
|
||||
suspend fun createDepositTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolDepositRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolDepositResponse>>
|
||||
|
||||
/**
|
||||
* Prepare unstake transaction
|
||||
*
|
||||
* Create unsigned transaction to initiate unstaking process.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Unstake parameters (staker public key, stake transaction hash)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/unstake")
|
||||
suspend fun createUnstakeTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolUnstakeRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolUnstakeResponse>>
|
||||
|
||||
/**
|
||||
* Prepare withdrawal transaction
|
||||
*
|
||||
* Create unsigned transaction to withdraw available funds from exit queue.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Withdrawal parameters (staker address)
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/staking/withdraw")
|
||||
suspend fun createWithdrawTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolWithdrawRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolWithdrawResponse>>
|
||||
|
||||
/**
|
||||
* Broadcast signed transaction
|
||||
*
|
||||
* Submit a signed transaction to the blockchain network.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param body Signed transaction in hexadecimal format
|
||||
*/
|
||||
@POST("api/v1/staking/pool/{network}/transaction/send")
|
||||
suspend fun broadcastTransaction(
|
||||
@Path("network") network: String,
|
||||
@Body body: P2PEthPoolBroadcastRequest,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolBroadcastResponse>>
|
||||
|
||||
/**
|
||||
* Get account summary
|
||||
*
|
||||
* Retrieve staking balance, rewards, and exit queue information for a specific account and vault.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param delegatorAddress Account address that initiated staking
|
||||
* @param vaultAddress Ethereum address of the vault
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}")
|
||||
suspend fun getAccountInfo(
|
||||
@Path("network") network: String,
|
||||
@Path("delegatorAddress") delegatorAddress: String,
|
||||
@Path("vaultAddress") vaultAddress: String,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolAccountResponse>>
|
||||
|
||||
/**
|
||||
* Get rewards history
|
||||
*
|
||||
* Retrieve historical rewards data for a specific account and vault.
|
||||
*
|
||||
* @param network Ethereum pool network: "mainnet" or "hoodi"
|
||||
* @param delegatorAddress Account address that initiated staking
|
||||
* @param vaultAddress Ethereum address of the vault
|
||||
* @param period Optional period filter (30, 60, or 90 days)
|
||||
*/
|
||||
@GET("api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards")
|
||||
suspend fun getRewards(
|
||||
@Path("network") network: String,
|
||||
@Path("delegatorAddress") delegatorAddress: String,
|
||||
@Path("vaultAddress") vaultAddress: String,
|
||||
@Query("period") period: Int? = null,
|
||||
): ApiResponse<P2PEthPoolResponse<P2PEthPoolRewardsResponse>>
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for broadcasting signed transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/transaction/send
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolBroadcastRequest(
|
||||
@Json(name = "signedTransaction")
|
||||
val signedTransaction: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for creating deposit transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/staking/deposit
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolDepositRequest(
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "amount")
|
||||
val amount: Double,
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for creating unstake transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/staking/unstake
|
||||
*
|
||||
* Note: Documentation seems to contain Bitcoin-related fields (possibly copy-paste error).
|
||||
* Using as-is per specification.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolUnstakeRequest(
|
||||
@Json(name = "stakerPublicKey")
|
||||
val stakerPublicKey: String,
|
||||
@Json(name = "stakeTransactionHash")
|
||||
val stakeTransactionHash: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.api.ethpool.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for creating withdrawal transaction
|
||||
*
|
||||
* Used in: POST /api/v1/staking/pool/{network}/staking/withdraw
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolWithdrawRequest(
|
||||
@Json(name = "stakerAddress")
|
||||
val stakerAddress: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Response for GET /api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolAccountResponse(
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "stake")
|
||||
val stake: P2PEthPoolStakeDTO,
|
||||
@Json(name = "availableToUnstake")
|
||||
val availableToUnstake: BigDecimal,
|
||||
@Json(name = "availableToWithdraw")
|
||||
val availableToWithdraw: BigDecimal,
|
||||
@Json(name = "exitQueue")
|
||||
val exitQueue: P2PEthPoolExitQueueDTO,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolStakeDTO(
|
||||
@Json(name = "assets")
|
||||
val assets: BigDecimal,
|
||||
@Json(name = "totalEarnedAssets")
|
||||
val totalEarnedAssets: BigDecimal,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolExitQueueDTO(
|
||||
@Json(name = "total")
|
||||
val total: Double,
|
||||
@Json(name = "requests")
|
||||
val requests: List<P2PEthPoolExitRequestDTO>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolExitRequestDTO(
|
||||
@Json(name = "ticket")
|
||||
val ticket: String,
|
||||
@Json(name = "totalAssets")
|
||||
val totalAssets: Double,
|
||||
@Json(name = "timestamp")
|
||||
val timestamp: Long,
|
||||
@Json(name = "withdrawalTimestamp")
|
||||
val withdrawalTimestamp: Long,
|
||||
@Json(name = "isClaimable")
|
||||
val isClaimable: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response for POST /api/v1/staking/pool/{network}/transaction/send
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolBroadcastResponse(
|
||||
@Json(name = "hash")
|
||||
val hash: String,
|
||||
@Json(name = "status")
|
||||
val status: P2PEthPoolTxStatusDTO,
|
||||
@Json(name = "blockNumber")
|
||||
val blockNumber: Int,
|
||||
@Json(name = "transactionIndex")
|
||||
val transactionIndex: Int,
|
||||
@Json(name = "gasUsed")
|
||||
val gasUsed: String,
|
||||
@Json(name = "cumulativeGasUsed")
|
||||
val cumulativeGasUsed: String,
|
||||
@Json(name = "effectiveGasPrice")
|
||||
val effectiveGasPrice: String?,
|
||||
@Json(name = "from")
|
||||
val from: String,
|
||||
@Json(name = "to")
|
||||
val to: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Transaction status from P2P API
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class P2PEthPoolTxStatusDTO {
|
||||
@Json(name = "success")
|
||||
SUCCESS,
|
||||
|
||||
@Json(name = "failed")
|
||||
FAILED,
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* Response for POST /api/v1/staking/pool/{network}/staking/deposit
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolDepositResponse(
|
||||
@Json(name = "amount")
|
||||
val amount: Double,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "unsignedTransaction")
|
||||
val unsignedTransaction: P2PEthPoolUnsignedTxDTO,
|
||||
@Json(name = "createdAt")
|
||||
val createdAt: DateTime,
|
||||
)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Error response structure for P2P.org API
|
||||
*
|
||||
* All P2P API endpoints return errors in this format
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolErrorResponse(
|
||||
@Json(name = "error")
|
||||
val error: P2PEthPoolErrorDetailsDTO,
|
||||
@Json(name = "result")
|
||||
val result: Any? = null, // null on error
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolErrorDetailsDTO(
|
||||
@Json(name = "code")
|
||||
val code: Int, // Error code (e.g., 127106, 101111)
|
||||
@Json(name = "message")
|
||||
val message: String, // Human-readable error message
|
||||
@Json(name = "name")
|
||||
val name: String, // Error name/type
|
||||
@Json(name = "errors")
|
||||
val errors: List<String>? = null, // Optional validation errors array
|
||||
)
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Unified response wrapper for all P2P.org API responses
|
||||
*
|
||||
* All P2P API endpoints return responses in this format:
|
||||
* ```json
|
||||
* {
|
||||
* "error": null | { code, message, name, errors },
|
||||
* "result": { ... } | null
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param T The type of the result data
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolResponse<T>(
|
||||
@Json(name = "error")
|
||||
val error: P2PEthPoolErrorDetailsDTO?,
|
||||
@Json(name = "result")
|
||||
val result: T?,
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Response for GET /api/v1/staking/pool/{network}/account/{delegatorAddress}/vault/{vaultAddress}/rewards
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolRewardsResponse(
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "rewards")
|
||||
val rewards: List<P2PEthPoolRewardDTO>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolRewardDTO(
|
||||
@Json(name = "date")
|
||||
val date: DateTime,
|
||||
@Json(name = "apy")
|
||||
val apy: Double,
|
||||
@Json(name = "balance")
|
||||
val balance: BigDecimal,
|
||||
@Json(name = "rewards")
|
||||
val rewards: BigDecimal,
|
||||
)
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Unsigned transaction structure
|
||||
*
|
||||
* Used in deposit, withdraw responses
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolUnsignedTxDTO(
|
||||
@Json(name = "serializeTx")
|
||||
val serializeTx: String,
|
||||
@Json(name = "to")
|
||||
val to: String,
|
||||
@Json(name = "data")
|
||||
val data: String,
|
||||
@Json(name = "value")
|
||||
val value: String,
|
||||
@Json(name = "nonce")
|
||||
val nonce: Int,
|
||||
@Json(name = "chainId")
|
||||
val chainId: Int,
|
||||
@Json(name = "gasLimit")
|
||||
val gasLimit: BigDecimal,
|
||||
@Json(name = "maxFeePerGas")
|
||||
val maxFeePerGas: BigDecimal,
|
||||
@Json(name = "maxPriorityFeePerGas")
|
||||
val maxPriorityFeePerGas: BigDecimal,
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response for POST /api/v1/staking/pool/{network}/staking/unstake
|
||||
*
|
||||
* Note: Contains Bitcoin-related fields (likely documentation error).
|
||||
* Using as-is per specification.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolUnstakeResponse(
|
||||
@Json(name = "stakerPublicKey")
|
||||
val stakerPublicKey: String,
|
||||
@Json(name = "stakeTransactionHash")
|
||||
val stakeTransactionHash: String,
|
||||
@Json(name = "unstakeTransactionHex")
|
||||
val unstakeTransactionHex: String, // unsigned
|
||||
@Json(name = "unstakeFee")
|
||||
val unstakeFee: Double,
|
||||
)
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* Response for GET /api/v1/staking/pool/{network}/vaults
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolVaultsResponse(
|
||||
@Json(name = "network")
|
||||
val network: P2PEthPoolNetworkDTO,
|
||||
@Json(name = "vaults")
|
||||
val vaults: List<P2PEthPoolVaultDTO>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Network identifier in P2P API
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class P2PEthPoolNetworkDTO {
|
||||
@Json(name = "mainnet")
|
||||
MAINNET,
|
||||
|
||||
@Json(name = "hoodi")
|
||||
HOODI,
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolVaultDTO(
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "displayName")
|
||||
val displayName: String,
|
||||
@Json(name = "apy")
|
||||
val apy: Double,
|
||||
@Json(name = "baseApy")
|
||||
val baseApy: Double,
|
||||
@Json(name = "capacity")
|
||||
val capacity: Double,
|
||||
@Json(name = "totalAssets")
|
||||
val totalAssets: Double,
|
||||
@Json(name = "feePercent")
|
||||
val feePercent: Double,
|
||||
@Json(name = "isPrivate")
|
||||
val isPrivate: Boolean,
|
||||
@Json(name = "isGenesis")
|
||||
val isGenesis: Boolean,
|
||||
@Json(name = "isSmoothingPool")
|
||||
val isSmoothingPool: Boolean,
|
||||
@Json(name = "isErc20")
|
||||
val isErc20: Boolean,
|
||||
@Json(name = "tokenName")
|
||||
val tokenName: String?,
|
||||
@Json(name = "tokenSymbol")
|
||||
val tokenSymbol: String?,
|
||||
@Json(name = "createdAt")
|
||||
val createdAt: DateTime,
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* Response for POST /api/v1/staking/pool/{network}/staking/withdraw
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PEthPoolWithdrawResponse(
|
||||
@Json(name = "amount")
|
||||
val amount: Double,
|
||||
@Json(name = "vaultAddress")
|
||||
val vaultAddress: String,
|
||||
@Json(name = "delegatorAddress")
|
||||
val delegatorAddress: String,
|
||||
@Json(name = "unsignedTransaction")
|
||||
val unsignedTransaction: P2PEthPoolUnsignedTxDTO,
|
||||
@Json(name = "createdAt")
|
||||
val createdAt: DateTime,
|
||||
@Json(name = "tickets")
|
||||
val tickets: List<String>,
|
||||
)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.datasource.api.express.models
|
||||
|
||||
object TangemExpressValues {
|
||||
const val EMPTY_CONTRACT_ADDRESS_VALUE = "0"
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.datasource.api.moonpay
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface MoonPayApi {
|
||||
|
||||
@GET("v4/ip_address/")
|
||||
suspend fun getUserStatus(@Query("apiKey") moonPayApiKey: String): MoonPayUserStatus
|
||||
|
||||
@GET("v3/currencies/")
|
||||
suspend fun getCurrencies(@Query("apiKey") moonPayApiKey: String): List<MoonPayCurrencies>
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MoonPayUserStatus(
|
||||
@Json(name = "isBuyAllowed")
|
||||
val isBuyAllowed: Boolean,
|
||||
@Json(name = "isSellAllowed")
|
||||
val isSellAllowed: Boolean,
|
||||
@Json(name = "isAllowed")
|
||||
val isMoonpayAllowed: Boolean,
|
||||
@Json(name = "alpha3")
|
||||
val countryCode: String,
|
||||
@Json(name = "state")
|
||||
val stateCode: String,
|
||||
)
|
||||
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MoonPayCurrencies(
|
||||
@Json(name = "type") val type: String,
|
||||
@Json(name = "code") val code: String,
|
||||
@Json(name = "supportsLiveMode") val supportsLiveMode: Boolean = false,
|
||||
@Json(name = "isSuspended") val isSuspended: Boolean = true,
|
||||
@Json(name = "isSupportedInUS") val isSupportedInUS: Boolean = false,
|
||||
@Json(name = "isSellSupported") val isSellSupported: Boolean = false,
|
||||
@Json(name = "notAllowedUSStates") val notAllowedUSStates: List<String> = emptyList(),
|
||||
@Json(name = "metadata") val metadata: MoonPayCurrenciesMetadata? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MoonPayCurrenciesMetadata(
|
||||
@Json(name = "contractAddress") val contractAddress: String?,
|
||||
@Json(name = "networkCode") val networkCode: String?,
|
||||
)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.datasource.api.news
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.news.models.response.NewsCategoriesResponse
|
||||
import com.tangem.datasource.api.news.models.response.NewsDetailsResponse
|
||||
import com.tangem.datasource.api.news.models.response.NewsListResponse
|
||||
import com.tangem.datasource.api.news.models.response.NewsTrendingResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface NewsApi {
|
||||
|
||||
@GET(NEWS_PATH)
|
||||
suspend fun getNews(
|
||||
@Query("page") page: Int? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("lang") language: String? = null,
|
||||
@Query("asOf") snapshot: String? = null,
|
||||
@Query("tokenIds") tokenIds: List<String>? = null,
|
||||
@Query("categoryIds") categoryIds: List<Int>? = null,
|
||||
): ApiResponse<NewsListResponse>
|
||||
|
||||
@GET("$NEWS_PATH/{newsId}")
|
||||
suspend fun getNewsDetails(
|
||||
@Path("newsId") newsId: Int,
|
||||
@Query("lang") language: String? = null,
|
||||
): ApiResponse<NewsDetailsResponse>
|
||||
|
||||
@GET("$NEWS_PATH/trending")
|
||||
suspend fun getTrendingNews(
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("lang") language: String? = null,
|
||||
): ApiResponse<NewsTrendingResponse>
|
||||
|
||||
@GET("$NEWS_PATH/categories")
|
||||
suspend fun getCategories(): ApiResponse<NewsCategoriesResponse>
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val NEWS_PATH = "api/v1/news"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.datasource.api.news.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsArticleDto(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "score") val score: Double,
|
||||
@Json(name = "language") val language: String,
|
||||
@Json(name = "isTrending") val isTrending: Boolean,
|
||||
@Json(name = "categories") val categories: List<NewsCategoryDto>,
|
||||
@Json(name = "relatedTokens") val relatedTokens: List<NewsRelatedTokenDto>,
|
||||
@Json(name = "title") val title: String,
|
||||
@Json(name = "newsUrl") val newsUrl: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsCategoryDto(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "name") val name: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsRelatedTokenDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "symbol") val symbol: String,
|
||||
@Json(name = "name") val name: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsOriginalArticleDto(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "title") val title: String,
|
||||
@Json(name = "sourceName") val sourceName: String,
|
||||
@Json(name = "language") val language: String,
|
||||
@Json(name = "publishedAt") val publishedAt: String,
|
||||
@Json(name = "url") val url: String,
|
||||
@Json(name = "imageUrl") val imageUrl: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.news.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsCategoriesResponse(
|
||||
@Json(name = "items") val items: List<NewsCategoryDto>,
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.datasource.api.news.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsDetailsResponse(
|
||||
@Json(name = "id") val id: Int,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "score") val score: Double,
|
||||
@Json(name = "language") val language: String,
|
||||
@Json(name = "isTrending") val isTrending: Boolean,
|
||||
@Json(name = "categories") val categories: List<NewsCategoryDto>,
|
||||
@Json(name = "relatedTokens") val relatedTokens: List<NewsRelatedTokenDto>,
|
||||
@Json(name = "title") val title: String,
|
||||
@Json(name = "newsUrl") val newsUrl: String,
|
||||
@Json(name = "shortContent") val shortContent: String,
|
||||
@Json(name = "content") val content: String,
|
||||
@Json(name = "originalArticles") val originalArticles: List<NewsOriginalArticleDto>,
|
||||
)
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.api.news.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsListResponse(
|
||||
@Json(name = "meta") val meta: NewsListMetaDto,
|
||||
@Json(name = "items") val items: List<NewsArticleDto>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsListMetaDto(
|
||||
@Json(name = "page") val page: Int,
|
||||
@Json(name = "limit") val limit: Int,
|
||||
@Json(name = "total") val total: Long,
|
||||
@Json(name = "hasNext") val hasNext: Boolean,
|
||||
@Json(name = "asOf") val asOf: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.api.news.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsTrendingResponse(
|
||||
@Json(name = "meta") val meta: NewsTrendingMetaDto,
|
||||
@Json(name = "items") val items: List<NewsArticleDto>,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NewsTrendingMetaDto(
|
||||
@Json(name = "limit") val limit: Int,
|
||||
)
|
||||
|
|
@ -7,6 +7,7 @@ import retrofit2.http.Body
|
|||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
|
|
@ -146,4 +147,22 @@ interface TangemPayApi {
|
|||
@Header("Authorization") authHeader: String,
|
||||
@Body body: CardDetailsRequest,
|
||||
): ApiResponse<CardDetailsResponse>
|
||||
|
||||
@PUT("v1/customer/card/pin")
|
||||
suspend fun setPin(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: SetPinRequest,
|
||||
): ApiResponse<SetPinResponse>
|
||||
|
||||
@POST("v1/customer/card/freeze")
|
||||
suspend fun freezeCard(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: FreezeUnfreezeCardRequest,
|
||||
): ApiResponse<FreezeUnfreezeCardResponse>
|
||||
|
||||
@POST("v1/customer/card/unfreeze")
|
||||
suspend fun unfreezeCard(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: FreezeUnfreezeCardRequest,
|
||||
): ApiResponse<FreezeUnfreezeCardResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FreezeUnfreezeCardRequest(@Json(name = "card_id") val cardId: String)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SetPinRequest(
|
||||
@Json(name = "pin") val pin: String,
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
@Json(name = "iv") val iv: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SetPinResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "result") val result: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -28,10 +28,49 @@ data class CustomerMeResponse(
|
|||
@Json(name = "cid") val cid: String,
|
||||
@Json(name = "card_id") val cardId: String,
|
||||
@Json(name = "card_wallet_address") val cardWalletAddress: String,
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "status") val status: Status,
|
||||
@Json(name = "updated_at") val updatedAt: String,
|
||||
@Json(name = "payment_account_id") val paymentAccountId: String,
|
||||
)
|
||||
) {
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Status {
|
||||
@Json(name = "new")
|
||||
NEW,
|
||||
|
||||
@Json(name = "ready_for_manufacturing")
|
||||
READY_FOR_MANUFACTURING,
|
||||
|
||||
@Json(name = "manufacturing")
|
||||
MANUFACTURING,
|
||||
|
||||
@Json(name = "sent_to_delivery")
|
||||
SENT_TO_DELIVERY,
|
||||
|
||||
@Json(name = "delivered")
|
||||
DELIVERED,
|
||||
|
||||
@Json(name = "activating")
|
||||
ACTIVATING,
|
||||
|
||||
@Json(name = "active")
|
||||
ACTIVE,
|
||||
|
||||
@Json(name = "blocked")
|
||||
BLOCKED,
|
||||
|
||||
@Json(name = "deactivating")
|
||||
DEACTIVATING,
|
||||
|
||||
@Json(name = "deactivated")
|
||||
DEACTIVATED,
|
||||
|
||||
@Json(name = "canceled")
|
||||
CANCELED,
|
||||
|
||||
@Json(name = "unknown")
|
||||
UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PaymentAccount(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class FreezeUnfreezeCardResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "order_id") val orderId: String,
|
||||
@Json(name = "status") val status: Status,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Status {
|
||||
@Json(name = "NEW")
|
||||
NEW,
|
||||
|
||||
@Json(name = "PROCESSING")
|
||||
PROCESSING,
|
||||
|
||||
@Json(name = "COMPLETED")
|
||||
COMPLETED,
|
||||
|
||||
@Json(name = "CANCELED")
|
||||
CANCELED,
|
||||
}
|
||||
}
|
||||
|
|
@ -137,6 +137,9 @@ interface TangemTechApi {
|
|||
|
||||
@GET("v1/user-wallets/wallets/by-app/{app_id}")
|
||||
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
|
||||
|
||||
@POST("v1/user-wallets/wallets")
|
||||
suspend fun createWallet(@Body body: WalletIdBody): ApiResponse<Unit>
|
||||
// endregion
|
||||
|
||||
// promo
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package com.tangem.datasource.api.tangemTech.models
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.common.extensions.calculateHashCode
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType.NONE
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.SortType
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
|
|
@ -68,4 +71,8 @@ data class UserTokensResponse(
|
|||
@Json(name = "marketcap")
|
||||
MARKETCAP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun GroupType?.orDefault(): GroupType = this ?: NONE
|
||||
|
||||
fun SortType?.orDefault(): SortType = this ?: SortType.MANUAL
|
||||
|
|
@ -7,5 +7,16 @@ import com.squareup.moshi.JsonClass
|
|||
data class WalletIdBody(
|
||||
@Json(name = "id") val walletId: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "cards") val cards: List<CardInfoBody>,
|
||||
)
|
||||
@Json(name = "type") val walletType: WalletType? = null,
|
||||
@Json(name = "cards") val cards: List<CardInfoBody>? = null,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class WalletType {
|
||||
@Json(name = "card")
|
||||
COLD,
|
||||
|
||||
@Json(name = "mobile")
|
||||
HOT,
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,23 @@ data class GetWalletAccountsResponse(
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Wallet(
|
||||
@Json(name = "version") val version: Int = 0,
|
||||
@Json(name = "group") val group: GroupType,
|
||||
@Json(name = "sort") val sort: SortType,
|
||||
@Json(name = "version") val version: Int? = 0,
|
||||
@Json(name = "group") val group: GroupType?,
|
||||
@Json(name = "sort") val sort: SortType?,
|
||||
@Json(name = "totalAccounts") val totalAccounts: Int,
|
||||
)
|
||||
}
|
||||
|
||||
/** Flattens the tokens from all wallet accounts into a single list */
|
||||
fun GetWalletAccountsResponse.flattenTokens(): List<UserTokensResponse.Token> {
|
||||
return accounts.flatMap { it.tokens.orEmpty() }
|
||||
}
|
||||
|
||||
/** Converts the [GetWalletAccountsResponse] into a [UserTokensResponse] */
|
||||
fun GetWalletAccountsResponse.toUserTokensResponse(): UserTokensResponse {
|
||||
return UserTokensResponse(
|
||||
group = wallet.group ?: GroupType.NONE,
|
||||
sort = wallet.sort ?: SortType.MANUAL,
|
||||
tokens = flattenTokens(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.accounts.AccountTokenMigrationStore
|
||||
import com.tangem.datasource.local.accounts.DefaultAccountTokenMigrationStore
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
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 AccountsMigrationStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountTokenMigrationStore(): AccountTokenMigrationStore {
|
||||
return DefaultAccountTokenMigrationStore(
|
||||
runtimeStateStore = RuntimeStateStore(emptyMap()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.datasource.api.common.AuthProvider
|
|||
import com.tangem.datasource.api.common.config.*
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
|
@ -39,6 +40,12 @@ internal object ApiConfigsModule {
|
|||
return StakeKit(stakeKitAuthProvider)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideP2PEthPoolConfig(p2pAuthProvider: P2PEthPoolAuthProvider): ApiConfig {
|
||||
return P2PEthPool(p2pAuthProvider)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemTechConfig(
|
||||
|
|
@ -51,6 +58,10 @@ internal object ApiConfigsModule {
|
|||
appInfoProvider = appInfoProvider,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideNewsConfig(authProvider: AuthProvider): ApiConfig = News(authProvider = authProvider)
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideYieldSupplyConfig(
|
||||
|
|
@ -74,4 +85,10 @@ internal object ApiConfigsModule {
|
|||
fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig {
|
||||
return BlockAid(environmentConfigStorage)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideMoonPayConfig(): ApiConfig {
|
||||
return MoonPay()
|
||||
}
|
||||
}
|
||||
|
|
@ -5,13 +5,17 @@ import com.tangem.datasource.api.common.blockaid.BlockAidApi
|
|||
import com.tangem.datasource.api.common.config.ApiConfig
|
||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
|
||||
import com.tangem.datasource.api.common.config.ApiConfigs
|
||||
import com.tangem.datasource.api.common.config.MoonPay
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager
|
||||
import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager
|
||||
import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.markets.TangemTechMarketsApi
|
||||
import com.tangem.datasource.api.moonpay.MoonPayApi
|
||||
import com.tangem.datasource.api.news.NewsApi
|
||||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
|
|
@ -71,6 +75,15 @@ internal object NetworkModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PEthPoolApi(retrofitApiBuilder: RetrofitApiBuilder): P2PEthPoolApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.P2PEthPool,
|
||||
applyTimeoutAnnotations = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOnrampApi(retrofitApiBuilder: RetrofitApiBuilder): OnrampApi {
|
||||
|
|
@ -130,4 +143,22 @@ internal object NetworkModule {
|
|||
applyTimeoutAnnotations = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMoonPayApi(retrofitApiBuilder: RetrofitApiBuilder): MoonPayApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.MoonPay,
|
||||
applyTimeoutAnnotations = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNewsApi(retrofitApiBuilder: RetrofitApiBuilder): NewsApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.News,
|
||||
applyTimeoutAnnotations = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.datasource.di.exchangeservice
|
||||
|
||||
import com.tangem.datasource.exchangeservice.swap.DefaultExpressServiceLoader
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ExchangeServiceLoaderModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindExpressServiceLoader(defaultExpressServiceLoader: DefaultExpressServiceLoader): ExpressServiceLoader
|
||||
}
|
||||
|
|
@ -197,6 +197,7 @@ internal class RetrofitApiBuilder @Inject constructor(
|
|||
|
||||
val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
|
||||
// ApiConfig.ID.StakeKit,
|
||||
ApiConfig.ID.MoonPay,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
package com.tangem.datasource.exchangeservice.swap
|
||||
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.request.AssetsRequestBody
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.express.models.response.Asset
|
||||
import com.tangem.datasource.exchangeservice.swap.ExpressUtils.getRefCode
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.token.ExpressAssetsStore
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.core.utils.lceContent
|
||||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
typealias InitializationStatusFlow = MutableStateFlow<Lce<Throwable, List<Asset>>>
|
||||
|
||||
/**
|
||||
* Default implementation of [ExpressServiceLoader]
|
||||
*
|
||||
* @property tangemExpressApi express api
|
||||
* @property expressAssetsStore local storage
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultExpressServiceLoader @Inject constructor(
|
||||
private val tangemExpressApi: TangemExpressApi,
|
||||
private val expressAssetsStore: ExpressAssetsStore,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : ExpressServiceLoader {
|
||||
|
||||
private val initializationStatuses =
|
||||
MutableStateFlow<Map<UserWalletId, InitializationStatusFlow>>(value = emptyMap())
|
||||
|
||||
override suspend fun update(userWallet: UserWallet, userTokens: List<LeastTokenInfo>) {
|
||||
withContext(dispatchers.io) {
|
||||
val initializationStatus = getInitializationStatusInternal(userWallet.walletId)
|
||||
|
||||
try {
|
||||
if (userTokens.isNotEmpty()) {
|
||||
val response = tangemExpressApi.getAssets(
|
||||
userWalletId = userWallet.walletId.stringValue,
|
||||
refCode = getRefCode(userWallet, appPreferencesStore),
|
||||
body = AssetsRequestBody(tokensList = userTokens),
|
||||
).getOrThrow()
|
||||
|
||||
expressAssetsStore.store(userWallet.walletId, response)
|
||||
|
||||
initializationStatus.update { response.lceContent() }
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (expressAssetsStore.getSyncOrNull(userWallet.walletId) == null) {
|
||||
initializationStatus.update { e.lceError() }
|
||||
}
|
||||
Timber.e(e, "Unable to fetch assets for: ${userWallet.walletId.stringValue}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>> {
|
||||
return flow { getInitializationStatusInternal(userWalletId).collect { emit(it) } }
|
||||
}
|
||||
|
||||
@Suppress("SuspendFunWithFlowReturnType")
|
||||
private suspend fun getInitializationStatusInternal(userWalletId: UserWalletId): InitializationStatusFlow {
|
||||
val initializationStatus = initializationStatuses.value[userWalletId]
|
||||
if (initializationStatus != null) return initializationStatus
|
||||
|
||||
val cached = expressAssetsStore.getSyncOrNull(userWalletId)
|
||||
val default: InitializationStatusFlow = MutableStateFlow(value = cached?.lceContent() ?: lceLoading())
|
||||
|
||||
initializationStatuses.update { statuses ->
|
||||
statuses.toMutableMap().apply {
|
||||
put(key = userWalletId, value = default)
|
||||
}
|
||||
}
|
||||
|
||||
return default
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package com.tangem.datasource.exchangeservice.swap
|
||||
|
||||
import com.tangem.datasource.api.express.models.request.LeastTokenInfo
|
||||
import com.tangem.datasource.api.express.models.response.Asset
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Express service loader
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface ExpressServiceLoader {
|
||||
|
||||
/** Update service using [userWallet] and [userTokens] */
|
||||
suspend fun update(userWallet: UserWallet, userTokens: List<LeastTokenInfo>)
|
||||
|
||||
/** Get initialization status by [userWalletId] */
|
||||
fun getInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, List<Asset>>>
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.local.accounts
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AccountTokenMigrationStore {
|
||||
|
||||
fun get(userWalletId: UserWalletId): Flow<Pair<String, String>?>
|
||||
|
||||
suspend fun store(userWalletId: UserWalletId, value: Pair<String, String>)
|
||||
|
||||
suspend fun remove(userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.local.accounts
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeStateStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class DefaultAccountTokenMigrationStore(
|
||||
private val runtimeStateStore: RuntimeStateStore<Map<UserWalletId, Pair<String, String>>>,
|
||||
) : AccountTokenMigrationStore {
|
||||
|
||||
override fun get(userWalletId: UserWalletId): Flow<Pair<String, String>?> {
|
||||
return runtimeStateStore.get().map { it[userWalletId] }
|
||||
}
|
||||
|
||||
override suspend fun store(userWalletId: UserWalletId, value: Pair<String, String>) {
|
||||
runtimeStateStore.update { it + (userWalletId to value) }
|
||||
}
|
||||
|
||||
override suspend fun remove(userWalletId: UserWalletId) {
|
||||
runtimeStateStore.update { it - userWalletId }
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ data class EnvironmentConfig(
|
|||
val express: ExpressModel? = null,
|
||||
val devExpress: ExpressModel? = null,
|
||||
val stakeKitApiKey: String? = null,
|
||||
// val p2pApiKey: P2PKeys? = null, TODO p2p after release 5.30
|
||||
val blockAidApiKey: String? = null,
|
||||
val tangemApiKey: String? = null,
|
||||
val tangemApiKeyDev: String? = null,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
|
|||
express = value.express,
|
||||
devExpress = value.devExpress,
|
||||
stakeKitApiKey = value.stakeKitApiKey,
|
||||
// p2pApiKey = value.p2pApiKey, TODO p2p after release 5.30
|
||||
blockAidApiKey = value.blockaidApiKey,
|
||||
tangemApiKey = value.tangemApiKey,
|
||||
tangemApiKeyDev = value.tangemApiKeyDev,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class EnvironmentConfigModel(
|
|||
@Json(name = "hederaArkhiaKey") val hederaArkhiaKey: String?,
|
||||
@Json(name = "polygonScanApiKey") val polygonScanApiKey: String?,
|
||||
@Json(name = "stakeKitApiKey") val stakeKitApiKey: String?,
|
||||
// @Json(name = "p2pApiKey") val p2pApiKey: P2PKeys?, TODO p2p after release 5.30
|
||||
@Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?,
|
||||
@Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?,
|
||||
@Json(name = "koinosProApiKey") val koinosProApiKey: String?,
|
||||
|
|
@ -97,6 +98,12 @@ data class TonCenterKeys(
|
|||
@Json(name = "testnet") val testnet: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PKeys(
|
||||
@Json(name = "mainnet") val mainnet: String,
|
||||
@Json(name = "hoodi") val hoodi: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetBlockToken(
|
||||
@Json(name = "jsonRpc") val jsonRPC: String?,
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ object PreferencesKeys {
|
|||
|
||||
val YIELD_SUPPLY_WARNINGS_STATES_KEY by lazy { stringPreferencesKey(name = "yieldSupplyWarningsStates") }
|
||||
|
||||
val ACCESS_CODE_SKIPPED_STATES_KEY by lazy { stringPreferencesKey(name = "accessCodeSkippedStates") }
|
||||
|
||||
// region Notifications
|
||||
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }
|
||||
|
||||
|
|
@ -171,6 +173,9 @@ object PreferencesKeys {
|
|||
fun getHotWalletUnlockDeadlineKey(attemptId: String) =
|
||||
longPreferencesKey(name = "hotWalletUnlockDeadline_$attemptId")
|
||||
|
||||
fun getTangemPayAddToWalletKey(customerWalletAddress: String) =
|
||||
booleanPreferencesKey("tangem_pay_add_to_wallet_done_key_$customerWalletAddress")
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
|
||||
interface TangemPayStorage {
|
||||
|
||||
suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String)
|
||||
suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String?
|
||||
|
||||
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens)
|
||||
|
||||
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
|
||||
|
|
@ -14,5 +18,9 @@ interface TangemPayStorage {
|
|||
|
||||
suspend fun clearOrderId(customerWalletAddress: String)
|
||||
|
||||
suspend fun clearAll(customerWalletAddress: String)
|
||||
suspend fun getAddToWalletDone(customerWalletAddress: String): Boolean
|
||||
|
||||
suspend fun storeAddToWalletDone(customerWalletAddress: String, isDone: Boolean)
|
||||
|
||||
suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String)
|
||||
}
|
||||
|
|
@ -71,6 +71,9 @@ class ApiConfigTest {
|
|||
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk())
|
||||
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = mockk())
|
||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk())
|
||||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk())
|
||||
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.datasource.api.common.config.managers
|
|||
|
||||
import android.os.Build
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.api.common.config.*
|
||||
|
|
@ -14,7 +13,9 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD
|
|||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
|
||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
|
|
@ -39,6 +40,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
private val appVersionProvider = mockk<AppVersionProvider>()
|
||||
private val expressAuthProvider = mockk<ExpressAuthProvider>()
|
||||
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
|
||||
private val p2pEthPoolAuthProvider = mockk<P2PEthPoolAuthProvider>()
|
||||
private val appAuthProvider = mockk<AuthProvider>()
|
||||
private val appInfoProvider = mockk<AppInfoProvider>()
|
||||
private val tangemApiKeyProvider = mockk<ProviderSuspend<String>>()
|
||||
|
|
@ -58,6 +60,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
every { appVersionProvider.versionName } returns VERSION_NAME
|
||||
every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID
|
||||
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
|
||||
every { p2pEthPoolAuthProvider.getApiKey() } returns P2P_API_KEY
|
||||
every { appAuthProvider.getApiKey(any()) } returns tangemApiKeyProvider
|
||||
coEvery { tangemApiKeyProvider.invoke() } returns TANGEM_API_KEY
|
||||
coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID
|
||||
|
|
@ -109,6 +112,9 @@ internal class ProdApiConfigsManagerTest {
|
|||
ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider)
|
||||
ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = appVersionProvider)
|
||||
ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage)
|
||||
ApiConfig.ID.MoonPay -> MoonPay()
|
||||
ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider)
|
||||
ApiConfig.ID.News -> News(authProvider = appAuthProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +127,9 @@ internal class ProdApiConfigsManagerTest {
|
|||
ApiConfig.ID.StakeKit -> createStakeKitModel()
|
||||
ApiConfig.ID.TangemPay -> createTangemPayModel()
|
||||
ApiConfig.ID.BlockAid -> createBlockAidSdkModel()
|
||||
ApiConfig.ID.MoonPay -> createMoonPayModel()
|
||||
ApiConfig.ID.P2PEthPool -> createP2PModel()
|
||||
ApiConfig.ID.News -> createNewsModel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -239,7 +248,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
id = ApiConfig.ID.TangemPay,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.DEV,
|
||||
baseUrl = "[REDACTED_ENV_URL]",
|
||||
baseUrl = "https://api.dev.us.paera.com/bff/",
|
||||
headers = mapOf(
|
||||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
|
|
@ -263,6 +272,54 @@ internal class ProdApiConfigsManagerTest {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createMoonPayModel(): TestModel {
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.MoonPay,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.moonpay.com/",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createP2PModel(): TestModel {
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.P2PEthPool,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.p2p.org/",
|
||||
headers = mapOf(
|
||||
"Authorization" to ProviderSuspend { "Bearer $P2P_API_KEY" },
|
||||
"accept" to ProviderSuspend { "application/json" },
|
||||
"Content-Type" to ProviderSuspend { "application/json" },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNewsModel(): TestModel {
|
||||
val (environment, baseUrl) = when (BuildConfig.BUILD_TYPE) {
|
||||
MOCKED_BUILD_TYPE,
|
||||
DEBUG_BUILD_TYPE,
|
||||
-> ApiEnvironment.DEV to "[REDACTED_ENV_URL]"
|
||||
INTERNAL_BUILD_TYPE,
|
||||
EXTERNAL_BUILD_TYPE,
|
||||
RELEASE_BUILD_TYPE,
|
||||
-> ApiEnvironment.PROD to "https://tangem.com/"
|
||||
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
|
||||
}
|
||||
return TestModel(
|
||||
id = ApiConfig.ID.News,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = environment,
|
||||
baseUrl = baseUrl,
|
||||
headers = mapOf(
|
||||
"api-key" to ProviderSuspend { TANGEM_API_KEY },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.checkHeaderValueOrEmpty(): String {
|
||||
for (i in this.indices) {
|
||||
val c = this[i]
|
||||
|
|
@ -281,6 +338,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
const val VERSION_NAME = "debug"
|
||||
const val EXPRESS_SESSION_ID = "express_session_id"
|
||||
const val STAKE_KIT_API_KEY = "stake_kit_api_key"
|
||||
const val P2P_API_KEY = "p2p_api_key"
|
||||
const val APP_CARD_ID = "app_card_id"
|
||||
const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.datasource.local.logs
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -12,7 +12,7 @@ import org.junit.jupiter.params.provider.MethodSource
|
|||
internal class LogsSanitizerTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideTestModels")
|
||||
@ProvideTestModels
|
||||
fun sanitize(model: TestModel) {
|
||||
// Act
|
||||
val actual = LogsSanitizer.sanitize(model.input)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.core.decompose.navigation
|
||||
|
||||
import com.arkivanov.decompose.ExperimentalDecomposeApi
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.router.stack.pushNew
|
||||
import com.arkivanov.decompose.router.stack.replaceCurrent
|
||||
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
|
@ -14,11 +14,14 @@ internal class DefaultRouter(
|
|||
private val navigation: StackNavigation<Route>
|
||||
get() = navigationProvider.getOrCreate()
|
||||
|
||||
@OptIn(ExperimentalDecomposeApi::class)
|
||||
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
navigation.pushNew(route, onComplete)
|
||||
}
|
||||
|
||||
override fun replaceCurrent(route: Route, onComplete: (Boolean) -> Unit) {
|
||||
navigation.replaceCurrent(route, { onComplete(true) })
|
||||
}
|
||||
|
||||
override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
val newRoutes = routes.toList()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ class DummyRouter : Router {
|
|||
onComplete(true)
|
||||
}
|
||||
|
||||
override fun replaceCurrent(route: Route, onComplete: (Boolean) -> Unit) {
|
||||
onComplete(true)
|
||||
}
|
||||
|
||||
override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
onComplete(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ interface Router {
|
|||
*/
|
||||
fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
|
||||
|
||||
/**
|
||||
* Replaces a current route with a new one.
|
||||
*
|
||||
* @param route The route to replace a current one.
|
||||
* @param onComplete The callback to be invoked when the operation is complete.
|
||||
*/
|
||||
fun replaceCurrent(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {})
|
||||
|
||||
/**
|
||||
* Replaces ***all*** routes in the navigation stack with the specified [routes].
|
||||
*
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ inline fun AppComponentContext.InnerPopRouter(
|
|||
router.push(route, onComplete)
|
||||
}
|
||||
|
||||
override fun replaceCurrent(route: Route, onComplete: (Boolean) -> Unit) {
|
||||
router.replaceCurrent(route, onComplete)
|
||||
}
|
||||
|
||||
override fun replaceAll(vararg routes: Route, onComplete: (Boolean) -> Unit) {
|
||||
router.replaceAll(*routes, onComplete = onComplete)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.core.decompose.navigation.inner
|
|||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.router.stack.pushNew
|
||||
import com.arkivanov.decompose.router.stack.replaceCurrent
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
|
|
@ -32,6 +33,14 @@ inline fun <reified T : Route> AppComponentContext.InnerRouter(
|
|||
}
|
||||
}
|
||||
|
||||
override fun replaceCurrent(route: Route, onComplete: (Boolean) -> Unit) {
|
||||
if (route is T) {
|
||||
stackNavigation.replaceCurrent(route, { onComplete(true) })
|
||||
} else {
|
||||
fallBackRouter.replaceCurrent(route, onComplete)
|
||||
}
|
||||
}
|
||||
|
||||
override fun replaceAll(vararg routes: Route, onComplete: (Boolean) -> Unit) {
|
||||
if (routes.any { it is T }) {
|
||||
val newRoutes = routes.toList().filterIsInstance<T>()
|
||||
|
|
|
|||
33
core/ui/detekt-baseline-debug.xml
Normal file
33
core/ui/detekt-baseline-debug.xml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:BigDecimalPercentFormat.kt$BigDecimalPercentFormat$val withPercentSign: Boolean = true</ID>
|
||||
<ID>BooleanPropertyNaming:FullScreen.kt$FullScreenLayout$private val focusable: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:TokenItemState.kt$TokenItemState.TitleState.Content$val earnApyIsActive: Boolean = false</ID>
|
||||
<ID>CanBeNonNullable:FooterContainer.kt$footer: TextReference? = null</ID>
|
||||
<ID>CanBeNonNullable:InputRowImageBase.kt$iconRes: Int?</ID>
|
||||
<ID>CanBeNonNullable:InputRowImageInfo.kt$subtitleEndIconRes: Int?</ID>
|
||||
<ID>CanBeNonNullable:SearchBar.kt$keyboardController: SoftwareKeyboardController?</ID>
|
||||
<ID>CanBeNonNullable:TextFields.kt$caption: String? = null</ID>
|
||||
<ID>CanBeNonNullable:TransactionList.kt$txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>?</ID>
|
||||
<ID>CastNullableToNonNullableType:DialogFullScreen.kt$as</ID>
|
||||
<ID>CastNullableToNonNullableType:FullScreen.kt$FullScreenLayout$as</ID>
|
||||
<ID>MultilineLambdaItParameter:Actions.kt${ ActionButtonContent( config = config, text = { textColor -> Text(text = config.text, textColor = textColor) }, modifier = it.padding( start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24, ), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:TangemDropdownMenu.kt${ if (it) { // Menu is expanded. 1f } else { // Menu is dismissed. 0.8f } }</ID>
|
||||
<ID>MultilineLambdaItParameter:TangemDropdownMenu.kt${ if (it) { // Menu is expanded. 1f } else { // Menu is dismissed. 0f } }</ID>
|
||||
<ID>NestedScopeFunctions:MessageBottomSheetUMV2.kt$apply(init)</ID>
|
||||
<ID>NestedScopeFunctions:Shadow.kt$apply { isDither = true isAntiAlias = true setShadowLayer( radiusPx, offset.x.toPx(), offset.y.toPx(), color.toArgb(), ) }</ID>
|
||||
<ID>NoNameShadowing:PinTextField.kt$value</ID>
|
||||
<ID>NoNameShadowing:SimpleTextField.kt$textStyle</ID>
|
||||
<ID>NoNameShadowing:TangemTheme.kt$systemUiController</ID>
|
||||
<ID>NoNameShadowing:TextAnimatedCounter.kt$char</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:InputManager.kt$InputManager$_query</ID>
|
||||
<ID>ReusedModifierInstance:EllipsisText.kt$Text( text = layoutText, color = color, style = style, fontStyle = fontStyle, textDecoration = textDecoration, textAlign = textAlign, softWrap = softWrap, maxLines = 1, onTextLayout = { textLayoutResultState.value = it }, modifier = modifier, )</ID>
|
||||
<ID>ReusedModifierInstance:Label.kt$Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = modifier .padding(horizontal = 4.dp) .clip(TangemTheme.shapes.roundedCorners8) .background(color = backgroundColor) .then( if (state.onClick != null) { Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = state.onClick, ) } else { Modifier }, ) .padding(horizontal = 8.dp, vertical = 4.dp), ) { Text( modifier = Modifier.weight(1.0f, fill = false), text = text.resolveReference(), style = TangemTheme.typography.caption1, color = textColor, ) AnimatedVisibility(state.icon != null) { val wrappedIcon = remember(this) { requireNotNull(state.icon) } Icon( imageVector = ImageVector.vectorResource(wrappedIcon), tint = iconColor, contentDescription = null, modifier = Modifier .size(16.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(bounded = false), onClick = { state.onIconClick?.invoke() }, ), ) } }</ID>
|
||||
<ID>ReusedModifierInstance:TangemRadioButton.kt$AnimatedVisibility( visible = isSelected, label = "Radio button animation", modifier = modifier .size(TangemTheme.dimens.size24), ) { Icon( painter = painterResource(id = R.drawable.ic_check_circle_24), contentDescription = null, tint = TangemTheme.colors.control.checked, ) }</ID>
|
||||
<ID>ReusedModifierInstance:TokenPrice.kt$Icon( modifier = modifier, painter = painterResource( id = when (animatedType) { PriceChangeType.UP -> R.drawable.ic_arrow_up_8 PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 }, ), tint = when (animatedType) { PriceChangeType.UP -> TangemTheme.colors.icon.accent PriceChangeType.DOWN -> TangemTheme.colors.icon.warning PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive }, contentDescription = null, )</ID>
|
||||
<ID>UnnecessaryEventHandlerParameter:PinTextField.kt$onValueChange: (String) -> Unit</ID>
|
||||
<ID>UnnecessaryEventHandlerParameter:ResizableText.kt$onFontSizeChange: (Float) -> Unit</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -6,18 +6,21 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -34,6 +37,7 @@ data class SmallButtonConfig(
|
|||
val onClick: () -> Unit,
|
||||
val icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
|
||||
val isEnabled: Boolean = true,
|
||||
val isLoading: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
@ -68,7 +72,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
|
|||
label = "Update background color",
|
||||
)
|
||||
|
||||
Row(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.defaultMinSize(
|
||||
minWidth = TangemTheme.dimens.size46,
|
||||
|
|
@ -79,7 +83,7 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
|
|||
color = backgroundColor,
|
||||
shape = shape,
|
||||
)
|
||||
.clickable(enabled = config.isEnabled, onClick = config.onClick)
|
||||
.clickable(enabled = !config.isLoading && config.isEnabled, onClick = config.onClick)
|
||||
.padding(
|
||||
paddingValues = when (config.icon) {
|
||||
is TangemButtonIconPosition.None -> PaddingValues(
|
||||
|
|
@ -95,42 +99,54 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier:
|
|||
)
|
||||
},
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
ContentContainer(
|
||||
iconPosition = config.icon,
|
||||
text = {
|
||||
val textColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
!config.isEnabled -> TangemTheme.colors.text.disabled
|
||||
isPrimary -> TangemTheme.colors.text.primary2
|
||||
else -> TangemTheme.colors.text.primary1
|
||||
},
|
||||
label = "Update text color",
|
||||
)
|
||||
if (config.isLoading) {
|
||||
CircularProgressIndicator(
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.conditional(config.isLoading) { alpha(0f) },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
ContentContainer(
|
||||
iconPosition = config.icon,
|
||||
text = {
|
||||
val textColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
!config.isEnabled -> TangemTheme.colors.text.disabled
|
||||
isPrimary -> TangemTheme.colors.text.primary2
|
||||
else -> TangemTheme.colors.text.primary1
|
||||
},
|
||||
label = "Update text color",
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing4),
|
||||
text = config.text.resolveReference(),
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.button,
|
||||
)
|
||||
},
|
||||
icon = { iconResId ->
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
painter = painterResource(id = iconResId),
|
||||
tint = if (config.isEnabled) {
|
||||
TangemTheme.colors.icon.secondary
|
||||
} else {
|
||||
TangemTheme.colors.icon.inactive
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing4),
|
||||
text = config.text.resolveReference(),
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.button,
|
||||
)
|
||||
},
|
||||
icon = { iconResId ->
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
painter = painterResource(id = iconResId),
|
||||
tint = if (config.isEnabled) {
|
||||
TangemTheme.colors.icon.secondary
|
||||
} else {
|
||||
TangemTheme.colors.icon.inactive
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +188,7 @@ private fun ButtonsSample() {
|
|||
)
|
||||
PrimarySmallButton(config = config)
|
||||
SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add")))
|
||||
SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add"), isLoading = true))
|
||||
SecondarySmallButton(
|
||||
config = config.copy(
|
||||
text = TextReference.Str(value = "Rating"),
|
||||
|
|
@ -191,5 +208,12 @@ private fun ButtonsSample() {
|
|||
isEnabled = false,
|
||||
),
|
||||
)
|
||||
SecondarySmallButton(
|
||||
config = config.copy(
|
||||
text = TextReference.Str(value = "Add token"),
|
||||
icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24),
|
||||
isLoading = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,8 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
|
@ -133,6 +135,13 @@ fun ActionBaseButton(
|
|||
}
|
||||
}
|
||||
.clip(shape)
|
||||
.semantics {
|
||||
contentDescription = if (config.shouldDimContent) {
|
||||
"Action button is dimmed"
|
||||
} else {
|
||||
"Action button is not dimmed"
|
||||
}
|
||||
}
|
||||
.combinedClickable(
|
||||
enabled = config.isEnabled,
|
||||
onClick = config.onClick,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.themedColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.PopUpMenuTestTags
|
||||
|
|
@ -28,7 +29,7 @@ fun TangemDropdownItem(item: TangemDropdownMenuItem, dismissParent: () -> Unit,
|
|||
.padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16)
|
||||
.testTag(PopUpMenuTestTags.BUTTON),
|
||||
text = item.title.resolveReference(),
|
||||
style = TangemTheme.typography.button.copy(color = item.textColorProvider()),
|
||||
style = TangemTheme.typography.button.copy(color = item.textColor.resolveReference()),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ private fun Preview_TokenDetailsAppBarDropdownItem() {
|
|||
dismissParent = {},
|
||||
item = TangemDropdownMenuItem(
|
||||
title = TextReference.Res(id = R.string.token_details_hide_token),
|
||||
textColorProvider = { TangemTheme.colors.text.warning },
|
||||
textColor = themedColor { TangemTheme.colors.text.warning },
|
||||
onClick = { },
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package com.tangem.core.ui.components.dropdownmenu
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.extensions.ColorReference
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
data class TangemDropdownMenuItem(
|
||||
val title: TextReference,
|
||||
val textColorProvider: @Composable () -> Color,
|
||||
val textColor: ColorReference,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.core.ui.components.feature
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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 androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp),
|
||||
painter = painterResource(iconRes),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp),
|
||||
text = description,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewFeatureBlock() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
FeatureBlock(
|
||||
title = stringResourceSafe(R.string.backup_info_save_title),
|
||||
description = stringResourceSafe(R.string.backup_info_save_description, "12"),
|
||||
iconRes = R.drawable.ic_lock_24,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
FeatureBlock(
|
||||
title = stringResourceSafe(R.string.backup_info_keep_title),
|
||||
description = stringResourceSafe(R.string.backup_info_keep_description),
|
||||
iconRes = R.drawable.ic_settings_24,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -134,13 +134,16 @@ fun InputRowRecipient(
|
|||
QrButton(
|
||||
visible = value.isBlank(),
|
||||
onQrCodeClick = onQrCodeClick,
|
||||
modifier = Modifier.testTag(SendAddressScreenTestTags.QR_BUTTON),
|
||||
)
|
||||
PasteButton(
|
||||
isPasteButtonVisible = value.isBlank(),
|
||||
onClick = onPasteClick,
|
||||
backgroundColorEnabled = TangemTheme.colors.button.secondary,
|
||||
textColor = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing8)
|
||||
.testTag(SendAddressScreenTestTags.ADDRESS_PASTE_BUTTON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -224,6 +227,7 @@ private fun ResolvedAddressRow(isLoading: Boolean, resolvedAddress: String?) {
|
|||
text = state.address,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.testTag(SendAddressScreenTestTags.RESOLVED_ADDRESS),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import com.tangem.core.ui.utils.getGreyScaleColorFilter
|
|||
* @param onImageError composable to show if image loading failed
|
||||
*/
|
||||
@Composable
|
||||
internal fun InputRowAsyncImage(
|
||||
fun InputRowAsyncImage(
|
||||
imageUrl: String,
|
||||
modifier: Modifier = Modifier,
|
||||
isGrayscale: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ fun NetworkTitle(
|
|||
Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing8))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f)
|
||||
.heightIn(min = TangemTheme.dimens.size24),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
content = action,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ fun SelectorRowItem(
|
|||
val textStyle = if (isSelected && showSelectedAppearance) {
|
||||
TangemTheme.typography.subtitle2
|
||||
} else {
|
||||
TangemTheme.typography.body2
|
||||
TangemTheme.typography.body1
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ private fun Preview_TooltipText() {
|
|||
) {
|
||||
TooltipText(
|
||||
text = stringReference("Text"),
|
||||
onInfoClick = { /* [REDACTED_TODO_COMMENT]*/ },
|
||||
onInfoClick = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
|
|||
import com.tangem.core.ui.components.token.internal.*
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.rememberHapticFeedback
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
|
|
@ -691,7 +692,10 @@ object AccountItemPreviewData {
|
|||
value = stringReference("24 tokens"),
|
||||
isAvailable = false,
|
||||
),
|
||||
subtitle2State = null,
|
||||
subtitle2State = Subtitle2State.PriceChangeContent(
|
||||
priceChangePercent = "0,43 %",
|
||||
type = PriceChangeType.UP,
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.audits.AuditLabel
|
||||
import com.tangem.core.ui.components.text.applyBladeBrush
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -31,7 +30,7 @@ internal fun TokenCryptoAmount(
|
|||
isFlickering = state.isFlickering,
|
||||
)
|
||||
}
|
||||
is TokenItemState.Subtitle2State.LabelContent -> {
|
||||
is TokenCryptoAmountState.LabelContent -> {
|
||||
AuditLabel(state = state.auditLabelUM, modifier = modifier)
|
||||
}
|
||||
is TokenCryptoAmountState.Unreachable -> {
|
||||
|
|
@ -46,6 +45,15 @@ internal fun TokenCryptoAmount(
|
|||
is TokenCryptoAmountState.Locked -> {
|
||||
LockedRectangle(modifier = modifier.placeholderSize())
|
||||
}
|
||||
is TokenCryptoAmountState.PriceChangeContent -> {
|
||||
PriceBlock(
|
||||
modifier = modifier,
|
||||
price = null,
|
||||
type = state.type,
|
||||
priceChangePercent = state.priceChangePercent,
|
||||
isFlickering = state.isFlickering,
|
||||
)
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ internal fun TokenPrice(state: TokenPriceState?, modifier: Modifier = Modifier)
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun PriceBlock(
|
||||
price: String,
|
||||
internal fun PriceBlock(
|
||||
price: String?,
|
||||
isFlickering: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
type: PriceChangeType? = null,
|
||||
|
|
@ -75,13 +75,15 @@ private fun PriceBlock(
|
|||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
PriceText(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
text = price,
|
||||
isFlickering = isFlickering,
|
||||
)
|
||||
if (price != null) {
|
||||
PriceText(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
text = price,
|
||||
isFlickering = isFlickering,
|
||||
)
|
||||
|
||||
SpacerW6()
|
||||
SpacerW6()
|
||||
}
|
||||
|
||||
if (type != null) {
|
||||
PriceChangeIcon(
|
||||
|
|
|
|||
|
|
@ -240,6 +240,12 @@ sealed class TokenItemState {
|
|||
val isFlickering: Boolean = false,
|
||||
) : Subtitle2State()
|
||||
|
||||
data class PriceChangeContent(
|
||||
val priceChangePercent: String,
|
||||
val type: PriceChangeType,
|
||||
val isFlickering: Boolean = false,
|
||||
) : Subtitle2State()
|
||||
|
||||
data class LabelContent(val auditLabelUM: AuditLabelUM) : Subtitle2State()
|
||||
|
||||
data object Unreachable : Subtitle2State()
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpandedPortfolioHeader(state: TokenItemState, isCollapsable: Boolean, modifier: Modifier = Modifier) {
|
||||
fun ExpandedPortfolioHeader(state: TokenItemState, isCollapsable: Boolean, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.core.ui.extensions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Utility class for keeping themed color reference from app theme.
|
||||
*
|
||||
* It necessary to use [Immutable] annotation for runtime stability.
|
||||
*
|
||||
* @property value color provider from theme
|
||||
*/
|
||||
@Immutable
|
||||
data class ColorReference(val value: @Composable () -> Color)
|
||||
|
||||
/**
|
||||
* Creates a [ColorReference] using a themed color from the app theme with a lambda.
|
||||
*
|
||||
* @param value The color provider from theme.
|
||||
* @return A [ColorReference] representing the themed color.
|
||||
*/
|
||||
fun themedColor(value: @Composable () -> Color): ColorReference {
|
||||
return ColorReference(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves [ColorReference] to [Color]
|
||||
*/
|
||||
@Composable
|
||||
fun ColorReference.resolveReference(): Color {
|
||||
return value()
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ object TangemColorPalette {
|
|||
|
||||
// region Light
|
||||
val Light1 = Color(0xFFF5F5F5)
|
||||
val Light1V2 = Color(0xFFF4F4F4)
|
||||
val Light2 = Color(0xFFEBEBEB)
|
||||
val Light3 = Color(0xFFD3D3D3)
|
||||
val Light4 = Color(0xFFC9C9C9)
|
||||
|
|
@ -27,6 +28,7 @@ object TangemColorPalette {
|
|||
// endregion Light
|
||||
|
||||
// region Green
|
||||
val Green = Color(0xFF0C9F3D)
|
||||
val Meadow = Color(0xFF1ACE80)
|
||||
val MagicMint = Color(0xFFA3EBCC)
|
||||
val DarkGreen = Color(0xFF06311F)
|
||||
|
|
@ -45,4 +47,9 @@ object TangemColorPalette {
|
|||
val Tangerine = Color(0xFFFFB71B)
|
||||
val Mustard = Color(0xFFFDDE55)
|
||||
// endregion Yellow
|
||||
|
||||
// region Overlay
|
||||
val Overlay1 = Color(0x66000000)
|
||||
val Overlay2 = Color(0xB2000000)
|
||||
// endregion Overlay
|
||||
}
|
||||
536
core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt
Normal file
536
core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt
Normal file
|
|
@ -0,0 +1,536 @@
|
|||
@file:Suppress("LongParameterList")
|
||||
package com.tangem.core.ui.res
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Stable
|
||||
class TangemColors2 internal constructor(
|
||||
val text: Text,
|
||||
val graphic: Graphic,
|
||||
val button: Button,
|
||||
val surface: Surface,
|
||||
val controls: Controls,
|
||||
val field: Field,
|
||||
val overlay: Overlay,
|
||||
val border: Border,
|
||||
val fill: Fill,
|
||||
val skeleton: Skeleton,
|
||||
val markers: Markers,
|
||||
) {
|
||||
|
||||
@Stable
|
||||
class Text internal constructor(
|
||||
val neutral: Neutral,
|
||||
val status: Status,
|
||||
) {
|
||||
@Stable
|
||||
class Neutral internal constructor(
|
||||
primary: Color,
|
||||
primaryInverted: Color,
|
||||
secondary: Color,
|
||||
tertiary: Color,
|
||||
primaryInvertedConstant: Color,
|
||||
) {
|
||||
var primary by mutableStateOf(primary)
|
||||
private set
|
||||
var primaryInverted by mutableStateOf(primaryInverted)
|
||||
private set
|
||||
var secondary by mutableStateOf(secondary)
|
||||
private set
|
||||
var tertiary by mutableStateOf(tertiary)
|
||||
private set
|
||||
var primaryInvertedConstant by mutableStateOf(primaryInvertedConstant)
|
||||
private set
|
||||
|
||||
fun update(other: Neutral) {
|
||||
primary = other.primary
|
||||
primaryInverted = other.primaryInverted
|
||||
secondary = other.secondary
|
||||
tertiary = other.tertiary
|
||||
primaryInvertedConstant = other.primaryInvertedConstant
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Status internal constructor(
|
||||
disabled: Color,
|
||||
accent: Color,
|
||||
warning: Color,
|
||||
attention: Color,
|
||||
positive: Color,
|
||||
) {
|
||||
var disabled by mutableStateOf(disabled)
|
||||
private set
|
||||
var accent by mutableStateOf(accent)
|
||||
private set
|
||||
var warning by mutableStateOf(warning)
|
||||
private set
|
||||
var attention by mutableStateOf(attention)
|
||||
private set
|
||||
var positive by mutableStateOf(positive)
|
||||
private set
|
||||
|
||||
fun update(other: Status) {
|
||||
disabled = other.disabled
|
||||
accent = other.accent
|
||||
warning = other.warning
|
||||
attention = other.attention
|
||||
positive = other.positive
|
||||
}
|
||||
}
|
||||
|
||||
fun update(other: Text) {
|
||||
neutral.update(other.neutral)
|
||||
status.update(other.status)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Graphic internal constructor(
|
||||
val neutral: Neutral,
|
||||
val status: Status,
|
||||
) {
|
||||
@Stable
|
||||
class Neutral internal constructor(
|
||||
primary: Color,
|
||||
primaryInverted: Color,
|
||||
secondary: Color,
|
||||
tertiary: Color,
|
||||
quaternary: Color,
|
||||
primaryInvertedConstant: Color,
|
||||
tertiaryConstant: Color,
|
||||
) {
|
||||
var primary by mutableStateOf(primary)
|
||||
private set
|
||||
var primaryInverted by mutableStateOf(primaryInverted)
|
||||
private set
|
||||
var secondary by mutableStateOf(secondary)
|
||||
private set
|
||||
var tertiary by mutableStateOf(tertiary)
|
||||
private set
|
||||
var quaternary by mutableStateOf(quaternary)
|
||||
private set
|
||||
var primaryInvertedConstant by mutableStateOf(primaryInvertedConstant)
|
||||
private set
|
||||
var tertiaryConstant by mutableStateOf(tertiaryConstant)
|
||||
private set
|
||||
|
||||
fun update(other: Neutral) {
|
||||
primary = other.primary
|
||||
primaryInverted = other.primaryInverted
|
||||
secondary = other.secondary
|
||||
tertiary = other.tertiary
|
||||
quaternary = other.quaternary
|
||||
primaryInvertedConstant = other.primaryInvertedConstant
|
||||
tertiaryConstant = other.tertiaryConstant
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Status internal constructor(
|
||||
accent: Color,
|
||||
warning: Color,
|
||||
attention: Color,
|
||||
) {
|
||||
var accent by mutableStateOf(accent)
|
||||
private set
|
||||
var warning by mutableStateOf(warning)
|
||||
private set
|
||||
var attention by mutableStateOf(attention)
|
||||
private set
|
||||
|
||||
fun update(other: Status) {
|
||||
accent = other.accent
|
||||
warning = other.warning
|
||||
attention = other.attention
|
||||
}
|
||||
}
|
||||
|
||||
fun update(other: Graphic) {
|
||||
neutral.update(other.neutral)
|
||||
status.update(other.status)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Button internal constructor(
|
||||
backgroundPrimary: Color,
|
||||
backgroundSecondary: Color,
|
||||
backgroundDisabled: Color,
|
||||
backgroundPositive: Color,
|
||||
textPrimary: Color,
|
||||
textSecondary: Color,
|
||||
textDisabled: Color,
|
||||
iconPrimary: Color,
|
||||
iconSecondary: Color,
|
||||
iconDisabled: Color,
|
||||
borderPrimary: Color,
|
||||
) {
|
||||
var backgroundPrimary by mutableStateOf(backgroundPrimary)
|
||||
private set
|
||||
var backgroundSecondary by mutableStateOf(backgroundSecondary)
|
||||
private set
|
||||
var backgroundDisabled by mutableStateOf(backgroundDisabled)
|
||||
private set
|
||||
var backgroundPositive by mutableStateOf(backgroundPositive)
|
||||
private set
|
||||
var textPrimary by mutableStateOf(textPrimary)
|
||||
private set
|
||||
var textSecondary by mutableStateOf(textSecondary)
|
||||
private set
|
||||
var textDisabled by mutableStateOf(textDisabled)
|
||||
private set
|
||||
var iconPrimary by mutableStateOf(iconPrimary)
|
||||
private set
|
||||
var iconSecondary by mutableStateOf(iconSecondary)
|
||||
private set
|
||||
var iconDisabled by mutableStateOf(iconDisabled)
|
||||
private set
|
||||
var borderPrimary by mutableStateOf(borderPrimary)
|
||||
private set
|
||||
|
||||
fun update(other: Button) {
|
||||
backgroundPrimary = other.backgroundPrimary
|
||||
backgroundSecondary = other.backgroundSecondary
|
||||
backgroundDisabled = other.backgroundDisabled
|
||||
backgroundPositive = other.backgroundPositive
|
||||
textPrimary = other.textPrimary
|
||||
textSecondary = other.textSecondary
|
||||
textDisabled = other.textDisabled
|
||||
iconPrimary = other.iconPrimary
|
||||
iconSecondary = other.iconSecondary
|
||||
iconDisabled = other.iconDisabled
|
||||
borderPrimary = other.borderPrimary
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Surface internal constructor(
|
||||
level1: Color,
|
||||
level2: Color,
|
||||
level3: Color,
|
||||
level4: Color,
|
||||
) {
|
||||
var level1 by mutableStateOf(level1)
|
||||
private set
|
||||
var level2 by mutableStateOf(level2)
|
||||
private set
|
||||
var level3 by mutableStateOf(level3)
|
||||
private set
|
||||
var level4 by mutableStateOf(level4)
|
||||
private set
|
||||
|
||||
fun update(other: Surface) {
|
||||
level1 = other.level1
|
||||
level2 = other.level2
|
||||
level3 = other.level3
|
||||
level4 = other.level4
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Controls internal constructor(
|
||||
backgroundDefault: Color,
|
||||
backgroundChecked: Color,
|
||||
iconDefault: Color,
|
||||
iconDisabled: Color,
|
||||
) {
|
||||
var backgroundDefault by mutableStateOf(backgroundDefault)
|
||||
private set
|
||||
var backgroundChecked by mutableStateOf(backgroundChecked)
|
||||
private set
|
||||
var iconDefault by mutableStateOf(iconDefault)
|
||||
private set
|
||||
var iconDisabled by mutableStateOf(iconDisabled)
|
||||
private set
|
||||
|
||||
fun update(other: Controls) {
|
||||
backgroundDefault = other.backgroundDefault
|
||||
backgroundChecked = other.backgroundChecked
|
||||
iconDefault = other.iconDefault
|
||||
iconDisabled = other.iconDisabled
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Field internal constructor(
|
||||
backgroundDefault: Color,
|
||||
backgroundFocused: Color,
|
||||
textPlaceholder: Color,
|
||||
textDefault: Color,
|
||||
textDisabled: Color,
|
||||
iconDefault: Color,
|
||||
iconDisabled: Color,
|
||||
textInvalid: Color,
|
||||
borderInvalid: Color,
|
||||
) {
|
||||
var backgroundDefault by mutableStateOf(backgroundDefault)
|
||||
private set
|
||||
var backgroundFocused by mutableStateOf(backgroundFocused)
|
||||
private set
|
||||
var textPlaceholder by mutableStateOf(textPlaceholder)
|
||||
private set
|
||||
var textDefault by mutableStateOf(textDefault)
|
||||
private set
|
||||
var textDisabled by mutableStateOf(textDisabled)
|
||||
private set
|
||||
var iconDefault by mutableStateOf(iconDefault)
|
||||
private set
|
||||
var iconDisabled by mutableStateOf(iconDisabled)
|
||||
private set
|
||||
var textInvalid by mutableStateOf(textInvalid)
|
||||
private set
|
||||
var borderInvalid by mutableStateOf(borderInvalid)
|
||||
private set
|
||||
|
||||
fun update(other: Field) {
|
||||
backgroundDefault = other.backgroundDefault
|
||||
backgroundFocused = other.backgroundFocused
|
||||
textPlaceholder = other.textPlaceholder
|
||||
textDefault = other.textDefault
|
||||
textDisabled = other.textDisabled
|
||||
iconDefault = other.iconDefault
|
||||
iconDisabled = other.iconDisabled
|
||||
textInvalid = other.textInvalid
|
||||
borderInvalid = other.borderInvalid
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Overlay internal constructor(
|
||||
overlayPrimary: Color,
|
||||
overlaySecondary: Color,
|
||||
) {
|
||||
var overlayPrimary by mutableStateOf(overlayPrimary)
|
||||
private set
|
||||
var overlaySecondary by mutableStateOf(overlaySecondary)
|
||||
private set
|
||||
|
||||
fun update(other: Overlay) {
|
||||
overlayPrimary = other.overlayPrimary
|
||||
overlaySecondary = other.overlaySecondary
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Border internal constructor(
|
||||
val neutral: Neutral,
|
||||
val status: Status,
|
||||
) {
|
||||
|
||||
@Stable
|
||||
class Neutral internal constructor(
|
||||
primary: Color,
|
||||
secondary: Color,
|
||||
) {
|
||||
var primary by mutableStateOf(primary)
|
||||
private set
|
||||
var secondary by mutableStateOf(secondary)
|
||||
private set
|
||||
|
||||
fun update(other: Neutral) {
|
||||
primary = other.primary
|
||||
secondary = other.secondary
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Status internal constructor(
|
||||
accent: Color,
|
||||
warning: Color,
|
||||
attention: Color,
|
||||
) {
|
||||
var accent by mutableStateOf(accent)
|
||||
private set
|
||||
var warning by mutableStateOf(warning)
|
||||
private set
|
||||
var attention by mutableStateOf(attention)
|
||||
private set
|
||||
|
||||
fun update(other: Status) {
|
||||
accent = other.accent
|
||||
warning = other.warning
|
||||
attention = other.attention
|
||||
}
|
||||
}
|
||||
|
||||
fun update(other: Border) {
|
||||
neutral.update(other.neutral)
|
||||
status.update(other.status)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Fill internal constructor(
|
||||
val neutral: Neutral,
|
||||
val status: Status,
|
||||
) {
|
||||
|
||||
@Stable
|
||||
class Neutral internal constructor(
|
||||
primary: Color,
|
||||
primaryInverted: Color,
|
||||
primaryInvertedConstant: Color,
|
||||
secondary: Color,
|
||||
tertiaryConstant: Color,
|
||||
quaternary: Color,
|
||||
) {
|
||||
var primary by mutableStateOf(primary)
|
||||
private set
|
||||
var primaryInverted by mutableStateOf(primaryInverted)
|
||||
private set
|
||||
var primaryInvertedConstant by mutableStateOf(primaryInvertedConstant)
|
||||
private set
|
||||
var secondary by mutableStateOf(secondary)
|
||||
private set
|
||||
var tertiaryConstant by mutableStateOf(tertiaryConstant)
|
||||
private set
|
||||
var quaternary by mutableStateOf(quaternary)
|
||||
private set
|
||||
|
||||
fun update(other: Neutral) {
|
||||
primary = other.primary
|
||||
primaryInverted = other.primaryInverted
|
||||
primaryInvertedConstant = other.primaryInvertedConstant
|
||||
secondary = other.secondary
|
||||
tertiaryConstant = other.tertiaryConstant
|
||||
quaternary = other.quaternary
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Status internal constructor(
|
||||
accent: Color,
|
||||
warning: Color,
|
||||
attention: Color,
|
||||
) {
|
||||
var accent by mutableStateOf(accent)
|
||||
private set
|
||||
var warning by mutableStateOf(warning)
|
||||
private set
|
||||
var attention by mutableStateOf(attention)
|
||||
private set
|
||||
|
||||
fun update(other: Status) {
|
||||
accent = other.accent
|
||||
warning = other.warning
|
||||
attention = other.attention
|
||||
}
|
||||
}
|
||||
|
||||
fun update(other: Fill) {
|
||||
neutral.update(other.neutral)
|
||||
status.update(other.status)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Skeleton internal constructor(
|
||||
backgroundPrimary: Color,
|
||||
) {
|
||||
var backgroundPrimary by mutableStateOf(backgroundPrimary)
|
||||
private set
|
||||
|
||||
fun update(other: Skeleton) {
|
||||
backgroundPrimary = other.backgroundPrimary
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class Markers internal constructor(
|
||||
backgroundSolidGray: Color,
|
||||
backgroundDisabled: Color,
|
||||
backgroundSolidBlue: Color,
|
||||
textGray: Color,
|
||||
textDisabled: Color,
|
||||
iconGray: Color,
|
||||
iconDisabled: Color,
|
||||
borderGray: Color,
|
||||
backgroundTintedBlue: Color,
|
||||
textBlue: Color,
|
||||
backgroundSolidRed: Color,
|
||||
backgroundTintedRed: Color,
|
||||
iconBlue: Color,
|
||||
iconRed: Color,
|
||||
textRed: Color,
|
||||
backgroundTintedGray: Color,
|
||||
borderTintedBlue: Color,
|
||||
borderTintedRed: Color,
|
||||
) {
|
||||
var backgroundSolidGray by mutableStateOf(backgroundSolidGray)
|
||||
private set
|
||||
var backgroundDisabled by mutableStateOf(backgroundDisabled)
|
||||
private set
|
||||
var backgroundSolidBlue by mutableStateOf(backgroundSolidBlue)
|
||||
private set
|
||||
var textGray by mutableStateOf(textGray)
|
||||
private set
|
||||
var textDisabled by mutableStateOf(textDisabled)
|
||||
private set
|
||||
var iconGray by mutableStateOf(iconGray)
|
||||
private set
|
||||
var iconDisabled by mutableStateOf(iconDisabled)
|
||||
private set
|
||||
var borderGray by mutableStateOf(borderGray)
|
||||
private set
|
||||
var backgroundTintedBlue by mutableStateOf(backgroundTintedBlue)
|
||||
private set
|
||||
var textBlue by mutableStateOf(textBlue)
|
||||
private set
|
||||
var backgroundSolidRed by mutableStateOf(backgroundSolidRed)
|
||||
private set
|
||||
var backgroundTintedRed by mutableStateOf(backgroundTintedRed)
|
||||
private set
|
||||
var iconBlue by mutableStateOf(iconBlue)
|
||||
private set
|
||||
var iconRed by mutableStateOf(iconRed)
|
||||
private set
|
||||
var textRed by mutableStateOf(textRed)
|
||||
private set
|
||||
var backgroundTintedGray by mutableStateOf(backgroundTintedGray)
|
||||
private set
|
||||
var borderTintedBlue by mutableStateOf(borderTintedBlue)
|
||||
private set
|
||||
var borderTintedRed by mutableStateOf(borderTintedRed)
|
||||
private set
|
||||
|
||||
fun update(other: Markers) {
|
||||
backgroundSolidGray = other.backgroundSolidGray
|
||||
backgroundDisabled = other.backgroundDisabled
|
||||
backgroundSolidBlue = other.backgroundSolidBlue
|
||||
textGray = other.textGray
|
||||
textDisabled = other.textDisabled
|
||||
iconGray = other.iconGray
|
||||
iconDisabled = other.iconDisabled
|
||||
borderGray = other.borderGray
|
||||
backgroundTintedBlue = other.backgroundTintedBlue
|
||||
textBlue = other.textBlue
|
||||
backgroundSolidRed = other.backgroundSolidRed
|
||||
backgroundTintedRed = other.backgroundTintedRed
|
||||
iconBlue = other.iconBlue
|
||||
iconRed = other.iconRed
|
||||
textRed = other.textRed
|
||||
backgroundTintedGray = other.backgroundTintedGray
|
||||
borderTintedBlue = other.borderTintedBlue
|
||||
borderTintedRed = other.borderTintedRed
|
||||
}
|
||||
}
|
||||
|
||||
fun update(other: TangemColors2) {
|
||||
text.update(other.text)
|
||||
graphic.update(other.graphic)
|
||||
button.update(other.button)
|
||||
surface.update(other.surface)
|
||||
controls.update(other.controls)
|
||||
field.update(other.field)
|
||||
overlay.update(other.overlay)
|
||||
border.update(other.border)
|
||||
fill.update(other.fill)
|
||||
skeleton.update(other.skeleton)
|
||||
markers.update(other.markers)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue