Updated on 2026-08-14
This commit is contained in:
commit
e8295055f0
1240 changed files with 39272 additions and 7385 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>
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "scroll",
|
||||
"name": "plasma",
|
||||
"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,59 @@
|
|||
package com.tangem.datasource.api.ethpool.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* 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: Long,
|
||||
)
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ data class TangemPayTxHistoryResponse(
|
|||
@Json(name = "memo") val memo: String? = null,
|
||||
@Json(name = "receipt") val receipt: Boolean,
|
||||
@Json(name = "merchant_name") val merchantName: String,
|
||||
@Json(name = "merchant_category") val merchantCategory: String,
|
||||
@Json(name = "merchant_category") val merchantCategory: String?,
|
||||
@Json(name = "merchant_category_code") val merchantCategoryCode: String,
|
||||
@Json(name = "merchant_id") val merchantId: String? = null,
|
||||
@Json(name = "enriched_merchant_icon") val enrichedMerchantIcon: String? = null,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.news.details.DefaultNewsDetailsStore
|
||||
import com.tangem.datasource.local.news.details.NewsDetailsStore
|
||||
import com.tangem.datasource.local.news.trending.DefaultTrendingNewsStore
|
||||
import com.tangem.datasource.local.news.trending.TrendingNewsStore
|
||||
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 NewsStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNewsDetailsStore(): NewsDetailsStore {
|
||||
return DefaultNewsDetailsStore(store = RuntimeSharedStore())
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTrendingNewsStore(): TrendingNewsStore {
|
||||
return DefaultTrendingNewsStore(store = RuntimeSharedStore())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore
|
||||
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
|
||||
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 TangemPayStoresModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayCardFrozenStateStore(): TangemPayCardFrozenStateStore {
|
||||
return DefaultTangemPayCardFrozenStateStore(
|
||||
dataStore = RuntimeDataStore(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.local.config.environment
|
|||
|
||||
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||
import com.tangem.datasource.local.config.environment.models.ExpressModel
|
||||
import com.tangem.datasource.local.config.environment.models.P2PKeys
|
||||
|
||||
data class EnvironmentConfig(
|
||||
val moonPayApiKey: String = "",
|
||||
|
|
@ -16,6 +17,7 @@ data class EnvironmentConfig(
|
|||
val express: ExpressModel? = null,
|
||||
val devExpress: ExpressModel? = null,
|
||||
val stakeKitApiKey: String? = null,
|
||||
val p2pApiKey: P2PKeys? = null,
|
||||
val blockAidApiKey: String? = null,
|
||||
val tangemApiKey: String? = null,
|
||||
val tangemApiKeyDev: String? = null,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel,
|
|||
apiKey = value.bscQuiknodeApiKey,
|
||||
subdomain = value.bscQuiknodeSubdomain,
|
||||
),
|
||||
quickNodePlasmaCredentials = QuickNodeCredentials(
|
||||
apiKey = value.quiknodeApiKey,
|
||||
subdomain = value.quiknodeSubdomain,
|
||||
),
|
||||
infuraProjectId = value.infuraProjectId,
|
||||
tronGridApiKey = value.tronGridApiKey,
|
||||
nowNodeCredentials = NowNodeCredentials(value.nowNodesApiKey),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
|
|||
express = value.express,
|
||||
devExpress = value.devExpress,
|
||||
stakeKitApiKey = value.stakeKitApiKey,
|
||||
p2pApiKey = value.p2pApiKey,
|
||||
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?,
|
||||
@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?,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
package com.tangem.datasource.local.datastore.core
|
||||
|
||||
@Deprecated(
|
||||
message = "Use RuntimeSharedStore instead",
|
||||
replaceWith = ReplaceWith("RuntimeSharedStore"),
|
||||
)
|
||||
internal interface StringKeyDataStore<Value : Any> : DataStore<String, Value>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.datasource.local.news.details
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.news.DetailedArticle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class DefaultNewsDetailsStore(
|
||||
private val store: RuntimeSharedStore<Map<Int, DetailedArticle>>,
|
||||
) : NewsDetailsStore {
|
||||
|
||||
override fun getAll(): Flow<List<DetailedArticle>> {
|
||||
return store.get().map { it.values.toList() }
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(id: Int): DetailedArticle? {
|
||||
return store.getSyncOrNull()?.get(id)
|
||||
}
|
||||
|
||||
override suspend fun store(id: Int, article: DetailedArticle) {
|
||||
store.update(emptyMap()) { current ->
|
||||
current + (id to article)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun store(articles: Map<Int, DetailedArticle>) {
|
||||
if (articles.isEmpty()) return
|
||||
|
||||
store.update(emptyMap()) { current ->
|
||||
current + articles
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
store.store(emptyMap())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.datasource.local.news.details
|
||||
|
||||
import com.tangem.domain.models.news.DetailedArticle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NewsDetailsStore {
|
||||
|
||||
fun getAll(): Flow<List<DetailedArticle>>
|
||||
|
||||
suspend fun getSyncOrNull(id: Int): DetailedArticle?
|
||||
|
||||
suspend fun store(id: Int, article: DetailedArticle)
|
||||
|
||||
suspend fun store(articles: Map<Int, DetailedArticle>)
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.datasource.local.news.trending
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private typealias TrendingCache = Map<String, List<ShortArticle>>
|
||||
|
||||
internal class DefaultTrendingNewsStore(
|
||||
private val store: RuntimeSharedStore<TrendingCache>,
|
||||
) : TrendingNewsStore {
|
||||
|
||||
override fun get(key: String): Flow<List<ShortArticle>> {
|
||||
return store.get().map { it[key].orEmpty() }
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(key: String): List<ShortArticle>? {
|
||||
return store.getSyncOrNull()?.get(key)
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, value: List<ShortArticle>) {
|
||||
store.update(emptyMap()) { current ->
|
||||
current + (key to value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clear() {
|
||||
store.store(emptyMap())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.local.news.trending
|
||||
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TrendingNewsStore {
|
||||
|
||||
fun get(key: String): Flow<List<ShortArticle>>
|
||||
|
||||
suspend fun getSyncOrNull(key: String): List<ShortArticle>?
|
||||
|
||||
suspend fun store(key: String, value: List<ShortArticle>)
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class DefaultTangemPayCardFrozenStateStore(
|
||||
private val dataStore: StringKeyDataStore<TangemPayCardFrozenState>,
|
||||
) : TangemPayCardFrozenStateStore {
|
||||
override suspend fun getSyncOrNull(key: String): TangemPayCardFrozenState? {
|
||||
return dataStore.getSyncOrNull(key)
|
||||
}
|
||||
|
||||
override fun get(key: String): Flow<TangemPayCardFrozenState> {
|
||||
return dataStore.get(key)
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, value: TangemPayCardFrozenState) {
|
||||
dataStore.store(key, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TangemPayCardFrozenStateStore {
|
||||
|
||||
suspend fun getSyncOrNull(key: String): TangemPayCardFrozenState?
|
||||
|
||||
fun get(key: String): Flow<TangemPayCardFrozenState>
|
||||
|
||||
suspend fun store(key: String, value: TangemPayCardFrozenState)
|
||||
}
|
||||
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@
|
|||
<string name="card_reset_alert_finish_message">All Tangem devices have been reset. You can now continue upgrading your wallet.</string>
|
||||
<string name="card_reset_alert_finish_ok_button">Upgrade again</string>
|
||||
<string name="card_reset_alert_finish_title">Reset complete</string>
|
||||
<string name="card_reset_alert_incomplete_message">You haven’t reset all your Tangem devices</string>
|
||||
<string name="card_reset_alert_incomplete_message">We recommend completing the reset process for all Tangem devices in this wallet.</string>
|
||||
<string name="card_reset_alert_incomplete_title">You haven’t reset all your Tangem devices</string>
|
||||
<string name="card_settings_access_code_recovery_disabled_description">Disable this option if you don\'t want this card to be used to reset access codes on other cards or rings in this wallet. Please note that this will also prevent you from resetting the access code on this card.</string>
|
||||
<string name="card_settings_access_code_recovery_enabled_description">Allows you to use this card to reset access code on other cards in this wallet</string>
|
||||
|
|
@ -275,6 +275,7 @@
|
|||
<string name="common_forget">Forget</string>
|
||||
<string name="common_free">Free</string>
|
||||
<string name="common_from">From</string>
|
||||
<string name="common_from_wallet_name">From %s</string>
|
||||
<string name="common_generate_addresses">Synchronize addresses</string>
|
||||
<string name="common_get_started">Get started</string>
|
||||
<string name="common_get_token">Get token</string>
|
||||
|
|
@ -300,12 +301,13 @@
|
|||
<item quantity="other">%d networks</item>
|
||||
</plurals>
|
||||
<string name="common_new_address">New address</string>
|
||||
<string name="common_news">News</string>
|
||||
<string name="common_next">Next</string>
|
||||
<string name="common_nft">NFT</string>
|
||||
<string name="common_no">No</string>
|
||||
<string name="common_no_address">No address</string>
|
||||
<string name="common_not_added">Not Added</string>
|
||||
<string name="common_not_now">Not Now</string>
|
||||
<string name="common_not_now">Not now</string>
|
||||
<string name="common_now">Now</string>
|
||||
<string name="common_ok">OK</string>
|
||||
<string name="common_open_in_browser">Open in Browser</string>
|
||||
|
|
@ -357,6 +359,7 @@
|
|||
<string name="common_terms_and_conditions">terms and conditions</string>
|
||||
<string name="common_terms_of_use">Terms of Use</string>
|
||||
<string name="common_to">To</string>
|
||||
<string name="common_to_wallet_name">To %s</string>
|
||||
<string name="common_today">Today</string>
|
||||
<plurals name="common_tokens_count">
|
||||
<item quantity="one">%d token</item>
|
||||
|
|
@ -519,6 +522,9 @@
|
|||
<string name="express_transaction_id">ID: %s</string>
|
||||
<string name="express_transaction_id_copied">Transaction ID copied</string>
|
||||
<string name="exсhange_token_description">Swap any asset in your portfolio for this token</string>
|
||||
<string name="feed_market_and_news">Market & News</string>
|
||||
<string name="feed_tangem_ai">Tangem AI</string>
|
||||
<string name="feed_trending_now">Trending Now</string>
|
||||
<string name="feedback_data_collection_message">The following information is optional. You can erase it if you don\'t want to share it.</string>
|
||||
<string name="feedback_preface_rate_negative">Tell us what functions you are missing, and we will try to help you.</string>
|
||||
<string name="feedback_preface_scan_failed">Please tell us what card or ring do you have</string>
|
||||
|
|
@ -605,7 +611,7 @@
|
|||
<string name="hw_remove_wallet_notification_title">Forget this wallet?</string>
|
||||
<string name="hw_remove_wallet_warning_access">I understand that if I haven\'t backed up my wallet before removing it, I may lose access to it.</string>
|
||||
<string name="hw_remove_wallet_warning_device">I understand that removing my wallet does not delete it—but simply removes it from my device.</string>
|
||||
<string name="hw_upgrade_backup_description">No seed phrase is needed anymore — your Tangem card or ring becomes your secure backup.</string>
|
||||
<string name="hw_upgrade_backup_description">No seed phrase is needed anymore. Your Tangem card or ring becomes your secure backup.</string>
|
||||
<string name="hw_upgrade_backup_title">Backup with Tangem</string>
|
||||
<string name="hw_upgrade_error_card_already_has_wallet">Can’t upgrade. A wallet already exists on this device.</string>
|
||||
<string name="hw_upgrade_error_card_key_import">Pick another device. This one can’t be used for the upgrade.</string>
|
||||
|
|
@ -772,6 +778,8 @@
|
|||
<string name="markets_token_details_volume">Volume</string>
|
||||
<string name="markets_tooltip_message">Pull this up or tap the search bar to add tokens directly from the market</string>
|
||||
<string name="markets_tooltip_title">Add tokens</string>
|
||||
<string name="news_all_news">All news</string>
|
||||
<string name="news_stay_in_the_loop">Stay in the loop</string>
|
||||
<string name="nfc_error_unavailable">NFC is not available on your device</string>
|
||||
<string name="nft_about_title">About NFT</string>
|
||||
<string name="nft_asset">NFT asset</string>
|
||||
|
|
@ -1070,7 +1078,7 @@
|
|||
<string name="send_alert_fee_too_low_text">You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue?</string>
|
||||
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
|
||||
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
|
||||
<string name="send_amount_convert_to_another_token">Convert to another token</string>
|
||||
<string name="send_amount_convert_to_another_token">Swap to another token or network</string>
|
||||
<string name="send_amount_label">Amount</string>
|
||||
<string name="send_amount_receive_token_subtitle">Will be sent to recipient</string>
|
||||
<string name="send_bitcoin_custom_fee_footer">You can set your transaction fee by adjusting the value in the Satoshi per vByte field.</string>
|
||||
|
|
@ -1651,7 +1659,7 @@
|
|||
<string name="wallet_create_scan_question">Using a Tangem Wallet already?</string>
|
||||
<string name="wallet_create_scan_title">Scan now</string>
|
||||
<string name="wallet_create_title">Pick a wallet setup method</string>
|
||||
<string name="wallet_import_buy_question">Ready to get a Tangem Wallet?</string>
|
||||
<string name="wallet_import_buy_question">Want to purchase Tangem Wallet?</string>
|
||||
<string name="wallet_import_buy_title">Buy now</string>
|
||||
<string name="wallet_import_google_drive_description">Recover existing wallet via Google Drive backup</string>
|
||||
<string name="wallet_import_google_drive_title">Import from Google Drive</string>
|
||||
|
|
@ -1673,7 +1681,7 @@
|
|||
<string name="wallet_promo_banner_button_title">Get now with 10% off</string>
|
||||
<string name="wallet_promo_banner_description">Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap.\nLink up to three cards for a backup.</string>
|
||||
<string name="wallet_promo_banner_title">Discover Tangem Wallet</string>
|
||||
<string name="wallet_settings_access_code_description">This secret code protects your wallet and is used to log in and sign transactions.</string>
|
||||
<string name="wallet_settings_access_code_description">This access code protects your wallet and is used to log in and sign transactions.</string>
|
||||
<string name="wallet_settings_access_code_title">Set/Change access code</string>
|
||||
<string name="wallet_settings_change_access_code_title">Change access code</string>
|
||||
<string name="wallet_settings_push_notifications_description">Stay notified on wallet incoming transactions and Tangem updates.</string>
|
||||
|
|
@ -1869,7 +1877,7 @@
|
|||
<string name="wc_uri_already_used_title">URI already used</string>
|
||||
<string name="wc_wallet_connect">WalletConnect</string>
|
||||
<string name="wc_warning_transaction">Suspicious transaction</string>
|
||||
<string name="welcome_create_wallet_already_have">Already have Tangem?</string>
|
||||
<string name="welcome_create_wallet_already_have">Already have Tangem Wallet?</string>
|
||||
<string name="welcome_create_wallet_feature_assets">Thousands of assets</string>
|
||||
<string name="welcome_create_wallet_feature_class">Best in class hardware wallet</string>
|
||||
<string name="welcome_create_wallet_feature_delivery">Fast delivery</string>
|
||||
|
|
@ -1878,7 +1886,7 @@
|
|||
<string name="welcome_create_wallet_feature_use">Simple to use</string>
|
||||
<string name="welcome_create_wallet_hardware_description">Create a hardware wallet with Tangem. Slim as a bank card, secure as a bank vault.</string>
|
||||
<string name="welcome_create_wallet_mobile_description">Create or import a software wallet</string>
|
||||
<string name="welcome_create_wallet_mobile_description_full">Create or import a software wallet on your phone </string>
|
||||
<string name="welcome_create_wallet_mobile_description_full">Create or import a software wallet on your phone.</string>
|
||||
<string name="welcome_create_wallet_mobile_title">Start with Mobile Wallet</string>
|
||||
<string name="welcome_create_wallet_other_method">Other method</string>
|
||||
<string name="welcome_create_wallet_use_hardware_description">Use Tangem Hardware Wallet</string>
|
||||
|
|
@ -1899,7 +1907,7 @@
|
|||
<string name="xtz_withdrawal_message_warning">To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ</string>
|
||||
<string name="yield_module_alert_description">When Yield Mode is active, all future top-ups to this address will be supplied to Aave. You can still manage your funds freely.</string>
|
||||
<string name="yield_module_alert_title">Your %s is supplied to Aave</string>
|
||||
<string name="yield_module_amount_not_transfered_to_aave_title">Supplying %1$s %2$s to Aave is pending</string>
|
||||
<string name="yield_module_amount_not_transfered_to_aave_title">Supplying %1$s %2$s to Aave</string>
|
||||
<string name="yield_module_approve_needed_notification_cta">Approve</string>
|
||||
<string name="yield_module_approve_needed_notification_description">Your token\'s approval has been revoked. Grant it again to resume the service\'s functionality.</string>
|
||||
<string name="yield_module_approve_needed_notification_title">Approve needed</string>
|
||||
|
|
@ -1975,9 +1983,9 @@
|
|||
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_title">Yield Mode</string>
|
||||
<string name="yield_module_token_details_earn_notification_processing">Enabling Yield Mode</string>
|
||||
<string name="yield_module_token_details_earn_notification_title">Yield Mode</string>
|
||||
<string name="yield_module_transaction_enter">Yield mode on</string>
|
||||
<string name="yield_module_transaction_exit">Yield mode off</string>
|
||||
<string name="yield_module_transaction_topup">Yield mode top-up</string>
|
||||
<string name="yield_module_transaction_enter">Yield Mode on</string>
|
||||
<string name="yield_module_transaction_exit">Yield Mode off</string>
|
||||
<string name="yield_module_transaction_topup">Yield Mode top-up</string>
|
||||
<string name="yield_module_transfer_mode_automatic">Automatic</string>
|
||||
<string name="yield_module_unable_to_cover_fee_description">Add some %1$s %2$s to cover the network fee for transactions.</string>
|
||||
<string name="yield_module_unable_to_cover_fee_title">Unable to cover %s fee</string>
|
||||
|
|
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.FooterTestTags
|
||||
|
||||
/**
|
||||
* Container for footer info below the text field
|
||||
|
|
@ -37,7 +39,8 @@ fun FooterContainer(
|
|||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(paddingValues),
|
||||
.padding(paddingValues)
|
||||
.testTag(FooterTestTags.FOOTER_TEXT),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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