Updated on 2026-08-14
This commit is contained in:
commit
20ac9bfad9
993 changed files with 31364 additions and 10416 deletions
|
|
@ -1,4 +1,6 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
import com.squareup.kotlinpoet.*
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import org.json.JSONArray
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
|
|
@ -9,8 +11,24 @@ plugins {
|
|||
id("configuration")
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("com.squareup:kotlinpoet:1.15.0")
|
||||
classpath("org.json:json:20231013")
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.configtoggle"
|
||||
sourceSets["main"].java.srcDir("build/generated/source/toggles")
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(generateFeatureToggles, generateExcludedBlockchainToggles)
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -32,7 +50,72 @@ dependencies {
|
|||
implementation(projects.core.utils)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
}
|
||||
|
||||
val generateFeatureToggles by tasks.registering {
|
||||
generateToggles(
|
||||
inputFilePath = "src/main/assets/configs/feature_toggles_config.json",
|
||||
generatedFileName = "FeatureToggles",
|
||||
)
|
||||
}
|
||||
|
||||
val generateExcludedBlockchainToggles by tasks.registering {
|
||||
generateToggles(
|
||||
inputFilePath = "src/main/assets/configs/excluded_blockchains_config.json",
|
||||
generatedFileName = "ExcludedBlockchainToggles",
|
||||
)
|
||||
}
|
||||
|
||||
fun Task.generateToggles(inputFilePath: String, generatedFileName: String) {
|
||||
val inputFile = file(inputFilePath)
|
||||
val outputDir = file("build/generated/source/toggles")
|
||||
|
||||
inputs.file(inputFile)
|
||||
outputs.dir(outputDir)
|
||||
|
||||
doLast {
|
||||
val jsonText = inputFile.readText()
|
||||
val jsonArray = JSONArray(jsonText)
|
||||
|
||||
val entries = (0 until jsonArray.length()).map { i ->
|
||||
val obj = jsonArray.getJSONObject(i)
|
||||
val name = obj.getString("name")
|
||||
val version = obj.getString("version")
|
||||
CodeBlock.of("%S to %S", name, version)
|
||||
}
|
||||
|
||||
val mapInitializer = CodeBlock.builder()
|
||||
.add("mapOf(\n")
|
||||
.indent()
|
||||
.apply {
|
||||
entries.forEachIndexed { index, entry ->
|
||||
add(entry)
|
||||
if (index != entries.lastIndex) add(",\n") else add("\n")
|
||||
}
|
||||
}
|
||||
.unindent()
|
||||
.add(")")
|
||||
.build()
|
||||
|
||||
val objectBuilder = TypeSpec.objectBuilder(name = generatedFileName)
|
||||
.addKdoc("Generated from $inputFilePath")
|
||||
.addProperty(
|
||||
PropertySpec.builder("values", MAP.parameterizedBy(STRING, STRING))
|
||||
.initializer(mapInitializer)
|
||||
.build()
|
||||
)
|
||||
|
||||
val fileSpec = FileSpec.builder(packageName = "com.tangem.core.configtoggle", fileName = generatedFileName)
|
||||
.addType(objectBuilder.build())
|
||||
.build()
|
||||
|
||||
val outputPackageDir = File(outputDir, "")
|
||||
outputPackageDir.mkdirs()
|
||||
fileSpec.writeTo(outputPackageDir)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,14 +19,6 @@
|
|||
"name": "STAKING_CARDANO_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "WALLET_CONNECT_REDESIGN_ENABLED",
|
||||
"version": "5.27.0"
|
||||
},
|
||||
{
|
||||
"name": "PUSH_NOTIFICATIONS_ENABLED",
|
||||
"version": "5.26.2"
|
||||
},
|
||||
{
|
||||
"name": "USEDESK_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -43,10 +35,6 @@
|
|||
"name": "SEND_REDESIGN_ENABLED",
|
||||
"version": "5.28.0"
|
||||
},
|
||||
{
|
||||
"name": "WALLET_BALANCE_FETCHER_ENABLED",
|
||||
"version": "5.27.0"
|
||||
},
|
||||
{
|
||||
"name": "HOT_WALLET_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -62,5 +50,17 @@
|
|||
{
|
||||
"name": "NEW_TOKEN_RECEIVE_ENABLED",
|
||||
"version": "5.28.0"
|
||||
},
|
||||
{
|
||||
"name": "YIELD_SUPPLY_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEW_ONRAMP_MAIN_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ACCOUNTS_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,4 @@ package com.tangem.core.configtoggle.blockchain
|
|||
interface ExcludedBlockchainsManager {
|
||||
|
||||
val excludedBlockchainsIds: Set<String>
|
||||
|
||||
suspend fun init()
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.storeObjectMap
|
||||
|
||||
internal class DefaultExcludedBlockchainsManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val versionProvider: VersionProvider,
|
||||
) : MutableExcludedBlockchainsManager {
|
||||
|
||||
private var isInitialized: Boolean = false
|
||||
|
||||
private lateinit var currentExcludedBlockchains: MutableMap<String, Boolean>
|
||||
private lateinit var localExcludedBlockchains: Map<String, Boolean>
|
||||
|
||||
override val excludedBlockchainsIds: Set<String>
|
||||
get() {
|
||||
if (!isInitialized) error("ExcludedBlockchainsManager is not initialized")
|
||||
|
||||
return currentExcludedBlockchains
|
||||
.filterValues { it }
|
||||
.keys
|
||||
}
|
||||
|
||||
override suspend fun init() {
|
||||
localTogglesStorage.populate(path = "configs/excluded_blockchains_config")
|
||||
|
||||
val storedExcludedBlockchainsIds = appPreferencesStore.getObjectMapSync<Boolean>(
|
||||
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
)
|
||||
|
||||
localExcludedBlockchains = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
||||
.mapValues { (_, isIncluded) -> !isIncluded }
|
||||
|
||||
currentExcludedBlockchains = (localExcludedBlockchains.keys + storedExcludedBlockchainsIds.keys)
|
||||
.fold(mutableMapOf()) { acc, blockchainId ->
|
||||
val isExcluded = storedExcludedBlockchainsIds[blockchainId] ?: localExcludedBlockchains[blockchainId]
|
||||
|
||||
requireNotNull(isExcluded) {
|
||||
"Unable to find $blockchainId in local or stored excluded blockchains"
|
||||
}
|
||||
|
||||
acc[blockchainId] = isExcluded
|
||||
acc
|
||||
}
|
||||
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) {
|
||||
currentExcludedBlockchains[mainnetId] = isExcluded
|
||||
|
||||
storeCurrent()
|
||||
}
|
||||
|
||||
override fun isMatchLocalConfig(): Boolean {
|
||||
return currentExcludedBlockchains == localExcludedBlockchains
|
||||
}
|
||||
|
||||
override suspend fun recoverLocalConfig() {
|
||||
currentExcludedBlockchains = localExcludedBlockchains.toMutableMap()
|
||||
|
||||
storeCurrent()
|
||||
}
|
||||
|
||||
private suspend fun storeCurrent() {
|
||||
appPreferencesStore.storeObjectMap(
|
||||
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
value = currentExcludedBlockchains,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.utils.toTableString
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* [MutableExcludedBlockchainsManager] implementation in dev or mocked build
|
||||
*
|
||||
* @property versionProvider application version provider
|
||||
* @property localTogglesStorage local storage for blockchain toggles
|
||||
*/
|
||||
internal class DevExcludedBlockchainsManager(
|
||||
private val versionProvider: VersionProvider,
|
||||
private val localTogglesStorage: LocalTogglesStorage,
|
||||
) : MutableExcludedBlockchainsManager {
|
||||
|
||||
private val fileBlockchainToggles: Map<String, Boolean> = getFileBlockchainToggles()
|
||||
private var blockchainTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
||||
|
||||
override val excludedBlockchainsIds: Set<String>
|
||||
get() = blockchainTogglesMap.filterValues { !it }.keys
|
||||
|
||||
init {
|
||||
val savedExcludedBlockchains = runBlocking { localTogglesStorage.getSyncOrEmpty() }
|
||||
|
||||
blockchainTogglesMap = fileBlockchainToggles
|
||||
.mapValues { (blockchainId, isEnabled) ->
|
||||
savedExcludedBlockchains[blockchainId] ?: isEnabled
|
||||
}
|
||||
.toMutableMap()
|
||||
}
|
||||
|
||||
override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) {
|
||||
blockchainTogglesMap[mainnetId] = isExcluded
|
||||
|
||||
localTogglesStorage.store(blockchainTogglesMap)
|
||||
}
|
||||
|
||||
override fun isMatchLocalConfig(): Boolean {
|
||||
return blockchainTogglesMap == fileBlockchainToggles
|
||||
}
|
||||
|
||||
override suspend fun recoverLocalConfig() {
|
||||
blockchainTogglesMap = fileBlockchainToggles.toMutableMap()
|
||||
|
||||
localTogglesStorage.store(blockchainTogglesMap)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return blockchainTogglesMap
|
||||
.filterKeys { it.isNotEmpty() }
|
||||
.toTableString(tableName = this@DevExcludedBlockchainsManager::class.java.simpleName)
|
||||
}
|
||||
|
||||
private fun getFileBlockchainToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return ExcludedBlockchainToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
|
||||
/**
|
||||
* [ExcludedBlockchainsManager] implementation in PROD build
|
||||
*
|
||||
* @property versionProvider application version provider
|
||||
*/
|
||||
internal class ProdExcludedBlockchainsManager(
|
||||
private val versionProvider: VersionProvider,
|
||||
) : ExcludedBlockchainsManager {
|
||||
|
||||
override val excludedBlockchainsIds: Set<String> = getBlockchainToggles()
|
||||
|
||||
private fun getBlockchainToggles(): Set<String> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return ExcludedBlockchainToggles.values
|
||||
.defineTogglesAvailability(appVersion = appVersion)
|
||||
.filterValues { !it }
|
||||
.keys
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,10 @@ package com.tangem.core.configtoggle.di
|
|||
import android.content.Context
|
||||
import com.tangem.core.configtoggle.BuildConfig
|
||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.DefaultExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.DevExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.ProdExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.DefaultVersionProvider
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -24,26 +23,20 @@ internal object ExcludedBlockchainsManagerModule {
|
|||
@Singleton
|
||||
fun provideExcludedBlockchainsManager(
|
||||
@ApplicationContext context: Context,
|
||||
assetLoader: AssetLoader,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
): ExcludedBlockchainsManager {
|
||||
val localTogglesStorage = LocalTogglesStorage(assetLoader)
|
||||
val versionProvider = DefaultVersionProvider(context)
|
||||
|
||||
return DefaultExcludedBlockchainsManager(
|
||||
localTogglesStorage,
|
||||
appPreferencesStore,
|
||||
versionProvider,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMutableExcludedBlockchainsManager(
|
||||
manager: ExcludedBlockchainsManager,
|
||||
): MutableExcludedBlockchainsManager? {
|
||||
if (!BuildConfig.TESTER_MENU_ENABLED) return null
|
||||
|
||||
return manager as MutableExcludedBlockchainsManager
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevExcludedBlockchainsManager(
|
||||
versionProvider = versionProvider,
|
||||
localTogglesStorage = LocalTogglesStorage(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
preferencesKey = LocalTogglesStorage.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ProdExcludedBlockchainsManager(versionProvider = versionProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,14 +7,12 @@ import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
|||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.DefaultVersionProvider
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -25,29 +23,20 @@ internal object FeatureTogglesManagerModule {
|
|||
@Singleton
|
||||
fun provideFeatureTogglesManager(
|
||||
@ApplicationContext context: Context,
|
||||
assetLoader: AssetLoader,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
): FeatureTogglesManager {
|
||||
val localTogglesStorage = LocalTogglesStorage(assetLoader)
|
||||
val versionProvider = DefaultVersionProvider(context)
|
||||
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
versionProvider = versionProvider,
|
||||
featureTogglesLocalStorage = LocalTogglesStorage(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
preferencesKey = LocalTogglesStorage.FEATURE_TOGGLES_KEY,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ProdFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
versionProvider = versionProvider,
|
||||
)
|
||||
}.also {
|
||||
// We need to initialize during the hilt graph creation
|
||||
// in order to provide the feature toggles correctly to other dependencies.
|
||||
runBlocking {
|
||||
it.init()
|
||||
}
|
||||
ProdFeatureTogglesManager(versionProvider = versionProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,6 @@ package com.tangem.core.configtoggle.feature
|
|||
*/
|
||||
interface FeatureTogglesManager {
|
||||
|
||||
/** Initialize manager */
|
||||
suspend fun init()
|
||||
|
||||
/** Check feature toggle availability by name [name] */
|
||||
fun isFeatureEnabled(name: String): Boolean
|
||||
}
|
||||
|
|
@ -1,77 +1,68 @@
|
|||
package com.tangem.core.configtoggle.feature.impl
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.utils.toTableString
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* Feature toggles manager implementation in DEV build
|
||||
* Feature toggles manager implementation in dev or mocked build
|
||||
*
|
||||
* @property localTogglesStorage local feature toggles storage
|
||||
* @property appPreferencesStore application local store
|
||||
* @property versionProvider application version provider
|
||||
* @property featureTogglesLocalStorage local storage for feature toggles
|
||||
*/
|
||||
internal class DevFeatureTogglesManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val versionProvider: VersionProvider,
|
||||
private val featureTogglesLocalStorage: LocalTogglesStorage,
|
||||
) : MutableFeatureTogglesManager {
|
||||
|
||||
private var featureTogglesMap: MutableMap<String, Boolean>? = null
|
||||
private var localFeatureTogglesMap: Map<String, Boolean>? = null
|
||||
private var fileFeatureTogglesMap: Map<String, Boolean> = getFileFeatureToggles()
|
||||
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
||||
|
||||
override suspend fun init() {
|
||||
if (featureTogglesMap != null && localFeatureTogglesMap != null) {
|
||||
return // Already initialized
|
||||
}
|
||||
init {
|
||||
val savedFeatureToggles = runBlocking { featureTogglesLocalStorage.getSyncOrEmpty() }
|
||||
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
|
||||
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
|
||||
) ?: emptyMap<String, Boolean>()
|
||||
|
||||
val localFeatureToggles = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
||||
|
||||
localFeatureTogglesMap = localFeatureToggles
|
||||
|
||||
featureTogglesMap = localFeatureToggles
|
||||
featureTogglesMap = fileFeatureTogglesMap
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureToggles[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
.toMutableMap()
|
||||
}
|
||||
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap!![name] ?: false
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] == true
|
||||
|
||||
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap
|
||||
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
|
||||
|
||||
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap!!
|
||||
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == fileFeatureTogglesMap
|
||||
|
||||
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
|
||||
featureTogglesMap!![name] ?: return
|
||||
featureTogglesMap!![name] = isEnabled
|
||||
appPreferencesStore.storeFeatureToggles(value = featureTogglesMap!!)
|
||||
featureTogglesMap[name] ?: return
|
||||
featureTogglesMap[name] = isEnabled
|
||||
featureTogglesLocalStorage.store(value = featureTogglesMap)
|
||||
}
|
||||
|
||||
override suspend fun recoverLocalConfig() {
|
||||
featureTogglesMap = localFeatureTogglesMap!!.toMutableMap()
|
||||
appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap!!)
|
||||
featureTogglesMap = fileFeatureTogglesMap.toMutableMap()
|
||||
featureTogglesLocalStorage.store(value = fileFeatureTogglesMap)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return featureTogglesMap.toTableString(tableName = this@DevFeatureTogglesManager::class.java.simpleName)
|
||||
}
|
||||
|
||||
private fun getFileFeatureToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun setFeatureToggles(map: MutableMap<String, Boolean>) {
|
||||
featureTogglesMap = map
|
||||
}
|
||||
|
||||
private suspend fun AppPreferencesStore.storeFeatureToggles(value: Map<String, Boolean>) {
|
||||
storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,30 @@
|
|||
package com.tangem.core.configtoggle.feature.impl
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
|
||||
/**
|
||||
* Feature toggles manager implementation in PROD build
|
||||
*
|
||||
* @property localTogglesStorage local feature toggles storage
|
||||
* @property versionProvider application version provider
|
||||
* @property versionProvider application version provider
|
||||
*/
|
||||
internal class ProdFeatureTogglesManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val versionProvider: VersionProvider,
|
||||
) : FeatureTogglesManager {
|
||||
|
||||
private var featureToggles: Map<String, Boolean>? = null
|
||||
private val featureToggles: Map<String, Boolean> = getFileFeatureToggles()
|
||||
|
||||
override suspend fun init() {
|
||||
if (featureToggles != null) {
|
||||
return // Already initialized
|
||||
}
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] == true
|
||||
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
featureToggles = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get() ?: "")
|
||||
private fun getFileFeatureToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureToggles!![name] ?: false
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun getProdFeatureToggles() = featureToggles!!
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun setProdFeatureToggles(map: Map<String, Boolean>) {
|
||||
featureToggles = map
|
||||
}
|
||||
fun getProdFeatureToggles() = featureToggles
|
||||
}
|
||||
|
|
@ -1,24 +1,34 @@
|
|||
package com.tangem.core.configtoggle.storage
|
||||
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import kotlin.properties.Delegates
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.storeObjectMap
|
||||
|
||||
/**
|
||||
* Storage implementation for storing local feature toggles.
|
||||
* Feature toggles are declared in file [LOCAL_CONFIG_PATH].
|
||||
* Local storage for toggles
|
||||
*
|
||||
* @property assetLoader asset loader
|
||||
* @property appPreferencesStore app preferences store
|
||||
* @property preferencesKey preferences key
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class LocalTogglesStorage(
|
||||
private val assetLoader: AssetLoader,
|
||||
) : TogglesStorage {
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val preferencesKey: Preferences.Key<String>,
|
||||
) {
|
||||
|
||||
override var toggles: List<ConfigToggle> by Delegates.notNull()
|
||||
private set
|
||||
suspend fun getSyncOrEmpty(): Map<String, Boolean> {
|
||||
return appPreferencesStore.getObjectMapSync<Boolean>(key = preferencesKey)
|
||||
}
|
||||
|
||||
override suspend fun populate(path: String) {
|
||||
toggles = assetLoader.loadList<ConfigToggle>(path)
|
||||
suspend fun store(value: Map<String, Boolean>) {
|
||||
appPreferencesStore.storeObjectMap(key = preferencesKey, value = value)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
|
||||
val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
package com.tangem.core.configtoggle.utils
|
||||
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.version.VersionAvailabilityContract
|
||||
|
||||
internal fun List<ConfigToggle>.associateToggles(currentVersion: String): Map<String, Boolean> {
|
||||
return associate { localToggle ->
|
||||
Pair(
|
||||
first = localToggle.name,
|
||||
second = VersionAvailabilityContract(currentVersion, localToggle.version),
|
||||
)
|
||||
internal fun Map<String, String>.defineTogglesAvailability(appVersion: String?): Map<String, Boolean> {
|
||||
return if (appVersion == null) {
|
||||
mapValues { false }
|
||||
} else {
|
||||
mapValues { (_, version) ->
|
||||
VersionAvailabilityContract(currentVersion = appVersion, localVersion = version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.core.configtoggle.utils
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
internal fun Map<String, Boolean>.toTableString(tableName: String): String {
|
||||
return buildString {
|
||||
append("$tableName:\n")
|
||||
append("|------------------------------------------|-----------|\n")
|
||||
append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", "name", "isEnabled"))
|
||||
append("|------------------------------------------|-----------|\n")
|
||||
entries.forEachIndexed { index, (name, isEnabled) ->
|
||||
append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", name, isEnabled))
|
||||
}
|
||||
append("|------------------------------------------|-----------|")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DevExcludedBlockchainsManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val localTogglesStorage = mockk<LocalTogglesStorage>(relaxUnitFun = true)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, localTogglesStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedToggles = mapOf("CHAIN_1" to false, "CHAIN_2" to true)
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns savedToggles
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns null
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2")
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if storage returns empty map`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_2")
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize manager if storage throws exception`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
val exception = Exception("Test exception")
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) }
|
||||
.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ExcludeBlockchain {
|
||||
|
||||
@Test
|
||||
fun excludeBlockchain_changesStatusAndSaves() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.excludeBlockchain("CHAIN_1", false)
|
||||
|
||||
// Act
|
||||
val actual = manager.excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).contains("CHAIN_1")
|
||||
coVerify { localTogglesStorage.store(match { it["CHAIN_1"] == false }) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsMatchLocalConfig {
|
||||
|
||||
@Test
|
||||
fun isMatchLocalConfig_returnsTrueIfMatchesFile() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
|
||||
// Act
|
||||
val actual = manager.isMatchLocalConfig()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isMatchLocalConfig_returnsFalseIfDiffersFromFile() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.excludeBlockchain("CHAIN_1", false)
|
||||
|
||||
// Act
|
||||
val actual = manager.isMatchLocalConfig()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class RecoverLocalConfig {
|
||||
|
||||
@Test
|
||||
fun recoverLocalConfig_resetsToFileAndSaves() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val toggles = mapOf("CHAIN_1" to "2.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns mapOf("CHAIN_1" to true)
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.recoverLocalConfig()
|
||||
|
||||
// Act
|
||||
val actual = manager.excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2")
|
||||
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
clearMocks(versionProvider, localTogglesStorage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ProdExcludedBlockchainsManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize excluded blockchains`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_2")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all blockchains excluded if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns null
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1", "CHAIN_2")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all blockchains excluded if versionProvider returns empty string`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns ""
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1", "CHAIN_2")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize if versionProvider throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test exception")
|
||||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { ProdExcludedBlockchainsManager(versionProvider) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,489 @@
|
|||
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 io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DevFeatureTogglesManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val featureTogglesLocalStorage = mockk<LocalTogglesStorage>(relaxUnitFun = true)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0")
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = null
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if versionProvider returns empty string`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = ""
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize manager if versionProvider throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test exception")
|
||||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) }
|
||||
.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
coVerify(inverse = true) { featureTogglesLocalStorage.getSyncOrEmpty() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if storage returns empty map`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = emptyMap<String, Boolean>()
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if storage returns unknown toggles`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = mapOf("TOGGLE_3" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize manager if storage throws exception`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val exception = Exception("Test exception")
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) }
|
||||
.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsFeatureEnabled {
|
||||
|
||||
private lateinit var manager: DevFeatureTogglesManager
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isFeatureEnabled(model: IsFeatureEnabledModel) {
|
||||
// Act
|
||||
val actual = manager.isFeatureEnabled(name = model.name)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = true),
|
||||
IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "", expected = false),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsMatchLocalConfig {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isMatchLocalConfig(model: IsMatchLocalConfigModel) = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns model.fileFeatureToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).apply {
|
||||
setFeatureToggles(model.storedFeatureToggles.toMutableMap())
|
||||
}
|
||||
|
||||
// Act
|
||||
val actual = manager.isMatchLocalConfig()
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false),
|
||||
expected = true,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_3" to true, "TOGGLE_4" to false),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = emptyMap(),
|
||||
storedFeatureToggles = emptyMap(),
|
||||
expected = true,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = emptyMap(),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true),
|
||||
expected = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class IsMatchLocalConfigModel(
|
||||
val fileFeatureToggles: Map<String, String>,
|
||||
val storedFeatureToggles: Map<String, Boolean>,
|
||||
val expected: Boolean,
|
||||
)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetFeatureToggles {
|
||||
|
||||
@Test
|
||||
fun getFeatureToggles() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val fileFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns fileFeatureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ChangeToggle {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun changeToggle(model: ChangeToggleModel) = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns model.initialToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
manager.changeToggle(name = model.name, isEnabled = model.isEnabled)
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = model.expectedToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
|
||||
if (model.expectedStoreSaving) {
|
||||
featureTogglesLocalStorage.store(model.expectedToggles)
|
||||
}
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_1",
|
||||
isEnabled = false,
|
||||
expectedToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false),
|
||||
expectedStoreSaving = true,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_2",
|
||||
isEnabled = true,
|
||||
expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true),
|
||||
expectedStoreSaving = true,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_3",
|
||||
isEnabled = true,
|
||||
expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false),
|
||||
expectedStoreSaving = false,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = emptyMap(),
|
||||
name = "TOGGLE_1",
|
||||
isEnabled = true,
|
||||
expectedToggles = emptyMap(),
|
||||
expectedStoreSaving = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class RecoverLocalConfig {
|
||||
|
||||
@Test
|
||||
fun recoverLocalConfig() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val fileFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns fileFeatureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
manager.recoverLocalConfig()
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
featureTogglesLocalStorage.store(expected)
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
}
|
||||
|
||||
data class ChangeToggleModel(
|
||||
val initialToggles: Map<String, String>,
|
||||
val name: String,
|
||||
val isEnabled: Boolean,
|
||||
val expectedToggles: Map<String, Boolean>,
|
||||
val expectedStoreSaving: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.collections.set
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
internal class DevTogglesManagerTest {
|
||||
|
||||
private val localTogglesStorage = mockk<TogglesStorage>()
|
||||
private val appPreferenceStore = AppPreferencesStore(
|
||||
moshi = Moshi.Builder().build(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
preferencesDataStore = mockk(relaxed = true),
|
||||
)
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val manager = DevFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
appPreferencesStore = appPreferenceStore,
|
||||
versionProvider = versionProvider,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns savedFeatureTogglesMap
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs kept empty list`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns emptyMap()
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns null
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues(Map.Entry<String, Boolean>::value)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns null
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion = "")
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle exists`() {
|
||||
val featureToggles = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED")
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle doesn't exists`() {
|
||||
val featureToggles = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "")
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFeatureToggles() {
|
||||
val expected = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(expected)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change toggle that contains in map`() = runTest {
|
||||
val changeableToggleName = "INACTIVE_TEST_FEATURE_ENABLED"
|
||||
val resultMap = mutableMapOf(
|
||||
changeableToggleName to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(resultMap)
|
||||
|
||||
manager.changeToggle(changeableToggleName, true)
|
||||
|
||||
resultMap[changeableToggleName] = true
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change toggle that doesn't contains in map`() = runTest {
|
||||
val resultMap = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(resultMap)
|
||||
|
||||
manager.changeToggle("FEATURE_TOGGLE", true)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val savedFeatureToggles = """
|
||||
[
|
||||
{
|
||||
"name": "INACTIVE_TEST_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
|
||||
val savedFeatureTogglesMap = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
val localFeatureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
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 io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ProdFeatureTogglesManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0")
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = null
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns empty string`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = ""
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if versionProvider throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test exception")
|
||||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { ProdFeatureTogglesManager(versionProvider) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsFeatureEnabled {
|
||||
|
||||
private lateinit var manager: ProdFeatureTogglesManager
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
|
||||
manager = ProdFeatureTogglesManager(versionProvider)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
clearMocks(versionProvider)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isFeatureEnabled(model: IsFeatureEnabledModel) {
|
||||
// Act
|
||||
val actual = manager.isFeatureEnabled(name = model.name)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = true),
|
||||
IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "", expected = false),
|
||||
)
|
||||
}
|
||||
|
||||
data class IsFeatureEnabledModel(val name: String, val expected: Boolean)
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class ProdTogglesManagerTest {
|
||||
|
||||
private val localTogglesStorage = mockk<TogglesStorage>()
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val manager = ProdFeatureTogglesManager(localTogglesStorage, versionProvider)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage`() = runTest {
|
||||
val currentVersion = "1.0.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles.associateToggles(currentVersion)
|
||||
Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } returns null
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(disabledFeatureToggles)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if localFeatureTogglesStorage throws exception`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } throws IllegalStateException(
|
||||
"Property featureToggles should be initialized before get.",
|
||||
)
|
||||
|
||||
runCatching { manager.init() }
|
||||
.onSuccess { throw IllegalStateException("localFeatureToggles shouldn't be initialized") }
|
||||
.onFailure {
|
||||
Truth
|
||||
.assertThat(it)
|
||||
.hasMessageThat()
|
||||
.contains("Property featureToggles should be initialized before get.")
|
||||
|
||||
Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java)
|
||||
}
|
||||
|
||||
coVerifyOrder { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) }
|
||||
verifyAll(inverse = true) { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if versionProvider throws exception`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } throws PackageManager.NameNotFoundException()
|
||||
|
||||
runCatching { manager.init() }
|
||||
.onSuccess { throw IllegalStateException("versionProvider should throws exception") }
|
||||
.onFailure {
|
||||
Truth.assertThat(it).isInstanceOf(PackageManager.NameNotFoundException::class.java)
|
||||
}
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle exists`() {
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setProdFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED")
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle doesn't exists`() {
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setProdFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "")
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val localFeatureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
|
||||
val disabledFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
package com.tangem.core.configtoggle.storage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
internal class LocalTogglesStorageTest {
|
||||
|
||||
private val assetReader = mockk<AssetReader>()
|
||||
private val moshi = mockk<Moshi>()
|
||||
private val jsonAdapter = mockk<JsonAdapter<List<ConfigToggle>>>()
|
||||
|
||||
// Impossible to mockk AssetLoader because it implement inline functions
|
||||
private val assetLoader = AssetLoader(
|
||||
assetReader = assetReader,
|
||||
moshi = moshi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val storage = LocalTogglesStorage(assetLoader)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage`() = runTest {
|
||||
everyReadingJson() returns json
|
||||
everyCreatingMoshiAdapter() returns jsonAdapter
|
||||
everyMappingJson() returns featureToggles
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder {
|
||||
assetReader.read(CONFIG_FILE_NAME)
|
||||
jsonAdapter.fromJson(json)
|
||||
}
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(featureToggles)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if assetReader throws exception`() = runTest {
|
||||
everyReadingJson() returns json
|
||||
everyCreatingMoshiAdapter() returns jsonAdapter
|
||||
everyMappingJson() throws IOException()
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder {
|
||||
assetReader.read(CONFIG_FILE_NAME)
|
||||
jsonAdapter.fromJson(json)
|
||||
}
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if jsonAdapter throws exception`() = runTest {
|
||||
everyReadingJson() throws IOException()
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) }
|
||||
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
|
||||
}
|
||||
|
||||
private fun everyReadingJson() = coEvery { assetReader.read(CONFIG_FILE_NAME) }
|
||||
|
||||
private fun everyCreatingMoshiAdapter() = every {
|
||||
val types = Types.newParameterizedType(List::class.java, ConfigToggle::class.java)
|
||||
moshi.adapter<List<ConfigToggle>>(types)
|
||||
}
|
||||
|
||||
private fun everyMappingJson() = every { jsonAdapter.fromJson(json) }
|
||||
|
||||
private companion object {
|
||||
const val CONFIG_FILE_NAME = "configs/feature_toggles_config.json"
|
||||
|
||||
val json = """
|
||||
[
|
||||
{
|
||||
"name": "INACTIVE_TEST_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
|
||||
val featureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,34 +7,51 @@ package com.tangem.datasource.api.common.response
|
|||
*/
|
||||
sealed class ApiResponse<T : Any> {
|
||||
|
||||
/**
|
||||
* Represents a successful response from the API.
|
||||
*
|
||||
* @property data The data returned by the API.
|
||||
*/
|
||||
data class Success<T : Any>(val data: T) : ApiResponse<T>()
|
||||
/** Map of headers (header name with list of values) */
|
||||
abstract val headers: Map<String, List<String>>
|
||||
|
||||
/**
|
||||
* Represents an error response or failure from the API.
|
||||
* Represents a successful response from the API
|
||||
*
|
||||
* @property cause The cause of the error.
|
||||
* @property data the data returned by the API
|
||||
* @property headers the headers returned by the API
|
||||
*/
|
||||
data class Error(val cause: ApiResponseError) : ApiResponse<Nothing>()
|
||||
data class Success<T : Any>(
|
||||
val data: T,
|
||||
override val headers: Map<String, List<String>> = emptyMap(),
|
||||
) : ApiResponse<T>()
|
||||
|
||||
/**
|
||||
* Represents an error response or failure from the API
|
||||
*
|
||||
* @property cause the cause of the error
|
||||
* @property headers the headers returned by the API
|
||||
*/
|
||||
data class Error(
|
||||
val cause: ApiResponseError,
|
||||
override val headers: Map<String, List<String>> = emptyMap(),
|
||||
) : ApiResponse<Nothing>()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps data in a [ApiResponse.Success] instance.
|
||||
* Wraps data in a [ApiResponse.Success] instance
|
||||
*
|
||||
* @param data The data to wrap.
|
||||
* @return A [ApiResponse.Success] instance containing the provided data.
|
||||
* @param data the data to wrap
|
||||
* @param headers the headers returned by the API
|
||||
* @return a [ApiResponse.Success] instance containing the provided data
|
||||
*/
|
||||
internal fun <T : Any> apiSuccess(data: T): ApiResponse<T> = ApiResponse.Success(data)
|
||||
internal fun <T : Any> apiSuccess(data: T, headers: Map<String, List<String>>): ApiResponse<T> {
|
||||
return ApiResponse.Success(data, headers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an [ApiResponseError] in a [ApiResponse.Error] instance.
|
||||
* Wraps an [ApiResponseError] in a [ApiResponse.Error] instance
|
||||
*
|
||||
* @param cause The error to wrap.
|
||||
* @return A [ApiResponse.Error] instance containing the provided error.
|
||||
* @param cause the error to wrap
|
||||
* @param headers the headers returned by the API
|
||||
* @return a [ApiResponse.Error] instance containing the provided error
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal fun <T : Any> apiError(cause: ApiResponseError): ApiResponse<T> = ApiResponse.Error(cause) as ApiResponse<T>
|
||||
internal fun <T : Any> apiError(cause: ApiResponseError, headers: Map<String, List<String>>): ApiResponse<T> {
|
||||
return ApiResponse.Error(cause, headers) as ApiResponse<T>
|
||||
}
|
||||
|
|
@ -47,7 +47,8 @@ internal class ApiResponseCallDelegate<T : Any>(
|
|||
Timber.e(e, "onFailure UnknownException")
|
||||
ApiResponseError.UnknownException(e)
|
||||
}
|
||||
val safeResponse = apiError<T>(error)
|
||||
|
||||
val safeResponse = apiError<T>(cause = error, headers = emptyMap())
|
||||
|
||||
responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ sealed class ApiResponseError : Exception() {
|
|||
|
||||
// region Error Codes
|
||||
enum class Code(val numericCode: Int) {
|
||||
// 3xx Server Errors
|
||||
NOT_MODIFIED(numericCode = 304),
|
||||
// 4xx Server Errors
|
||||
BAD_REQUEST(numericCode = 400),
|
||||
UNAUTHORIZED(numericCode = 401),
|
||||
|
|
@ -64,10 +66,6 @@ sealed class ApiResponseError : Exception() {
|
|||
;
|
||||
|
||||
override fun toString(): String = "$numericCode - $name"
|
||||
|
||||
companion object {
|
||||
val values = values()
|
||||
}
|
||||
}
|
||||
// endregion Error Codes
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,4 +19,9 @@ inline fun <T> catchApiResponseError(onError: (ApiResponseError) -> Unit, block:
|
|||
onError(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks if the ApiResponseError is a network error with the specified HTTP status [code] */
|
||||
fun ApiResponseError.isNetworkError(code: ApiResponseError.HttpException.Code): Boolean {
|
||||
return this is ApiResponseError.HttpException && this.code == code
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
const val IF_NONE_MATCH_HEADER = "IfNoneMatch"
|
||||
|
|
@ -12,12 +12,13 @@ import java.util.concurrent.TimeoutException
|
|||
import javax.net.ssl.SSLHandshakeException
|
||||
|
||||
internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: AnalyticsErrorHandler): ApiResponse<T> {
|
||||
val headers = headers().toMultimap()
|
||||
val body = body()
|
||||
|
||||
return if (isSuccessful && body != null) {
|
||||
apiSuccess(body)
|
||||
apiSuccess(data = body, headers = headers)
|
||||
} else {
|
||||
val code = ApiResponseError.HttpException.Code.values
|
||||
val code = ApiResponseError.HttpException.Code.entries
|
||||
.firstOrNull { it.numericCode == code() }
|
||||
val e = try {
|
||||
if (code == null) {
|
||||
|
|
@ -33,7 +34,7 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: Anal
|
|||
ApiResponseError.UnknownException(e)
|
||||
}
|
||||
|
||||
apiError(e)
|
||||
apiError(e, headers)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ data class TxDetails(
|
|||
val txData: String?, // transaction data if DEX, null if CEX
|
||||
|
||||
@Json(name = "txValue")
|
||||
val txValue: String, // amount (same as fromAmount for Coin, but for bridge equal to otherNativeFee)
|
||||
val txValue: String?, // amount (same as fromAmount for Coin, but for bridge equal to otherNativeFee)
|
||||
|
||||
@Json(name = "otherNativeFee")
|
||||
val otherNativeFee: String?,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import retrofit2.http.Header
|
|||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TangemPayApi {
|
||||
|
||||
// region: auth
|
||||
|
|
@ -32,6 +33,11 @@ interface TangemPayApi {
|
|||
@POST("v1/auth/token")
|
||||
suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse<JWTResponse>
|
||||
|
||||
@POST("v1/auth/token/refresh")
|
||||
suspend fun refreshCustomerWalletAccessToken(
|
||||
@Body request: RefreshCustomerWalletAccessTokenRequest,
|
||||
): ApiResponse<JWTResponse>
|
||||
|
||||
@POST("v1/auth/token")
|
||||
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>
|
||||
|
||||
|
|
@ -103,4 +109,10 @@ interface TangemPayApi {
|
|||
|
||||
@GET("v1/customer/kyc")
|
||||
suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse<KycAccessInfoResponse>
|
||||
|
||||
@GET("v1/customer/me")
|
||||
suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse>
|
||||
|
||||
@POST("v1/deeplink/validate")
|
||||
suspend fun validateDeeplink(@Body body: DeeplinkValidityRequest): ApiResponse<DeeplinkValidityResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DeeplinkValidityRequest(
|
||||
@Json(name = "link") val link: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RefreshCustomerWalletAccessTokenRequest(
|
||||
@Json(name = "auth_type") val authType: String = "customer_wallet",
|
||||
@Json(name = "refresh_token") val refreshToken: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerMeResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "state") val state: String,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "product_instance") val productInstance: ProductInstance?,
|
||||
@Json(name = "payment_account") val paymentAccount: PaymentAccount?,
|
||||
@Json(name = "kyc") val kyc: Kyc?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ProductInstance(
|
||||
@Json(name = "id") val id: String,
|
||||
@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 = "updated_at") val updatedAt: String,
|
||||
@Json(name = "payment_account_id") val paymentAccountId: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PaymentAccount(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "address") val address: String,
|
||||
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Kyc(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "provider") val provider: String,
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "risk") val risk: String,
|
||||
@Json(name = "review_answer") val reviewAnswer: String,
|
||||
@Json(name = "created_at") val createdAt: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DeeplinkValidityResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "status") val status: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -119,9 +119,6 @@ interface TangemTechApi {
|
|||
@Path("application_id") applicationId: String,
|
||||
@Body body: NotificationApplicationCreateBody,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@PATCH("v1/user-wallets/wallets/{wallet_id}/notify")
|
||||
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
|
||||
// endregion
|
||||
|
||||
// region user-wallets
|
||||
|
|
@ -148,17 +145,22 @@ interface TangemTechApi {
|
|||
|
||||
// region account
|
||||
@GET("/v1/wallets/{walletId}/accounts")
|
||||
suspend fun getWalletAccounts(@Path("walletId") walletId: String): ApiResponse<GetWalletAccountsResponse>
|
||||
suspend fun getWalletAccounts(
|
||||
@Path("walletId") walletId: String,
|
||||
@Header("If-None-Match") eTag: String? = null,
|
||||
): ApiResponse<GetWalletAccountsResponse>
|
||||
|
||||
@PUT("/v1/wallets/{walletId}/accounts")
|
||||
suspend fun saveWalletAccounts(
|
||||
@Path("walletId") walletId: String,
|
||||
@Header("If-Match") ifMatch: String,
|
||||
): ApiResponse<SaveWalletAccountsResponse>
|
||||
@Header("If-Match") eTag: String,
|
||||
@Body body: SaveWalletAccountsResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@GET("/v1/wallets/{walletId}/accounts/archived")
|
||||
suspend fun getWalletArchivedAccounts(
|
||||
@Path("walletId") walletId: String,
|
||||
@Header("If-None-Match") eTag: String? = null,
|
||||
): ApiResponse<GetWalletArchivedAccountsResponse>
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ data class GetWalletAccountsResponse(
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Wallet(
|
||||
@Json(name = "version") val version: Int,
|
||||
@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,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class WalletAccountDTO(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "name") val name: String?,
|
||||
@Json(name = "derivation") val derivationIndex: Int,
|
||||
@Json(name = "icon") val icon: String,
|
||||
@Json(name = "iconColor") val iconColor: String,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.datasource.local.walletconnect.DefaultWalletConnectStore
|
|||
import com.tangem.datasource.local.walletconnect.WalletConnectStore
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.setTypes
|
||||
import com.tangem.domain.walletconnect.model.WcPendingApprovalSessionDTO
|
||||
import com.tangem.domain.walletconnect.model.WcSessionDTO
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -30,6 +31,7 @@ object WalletConnectModule {
|
|||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): WalletConnectStore {
|
||||
val scope = CoroutineScope(context = dispatchers.io + SupervisorJob())
|
||||
return DefaultWalletConnectStore(
|
||||
persistenceStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
|
|
@ -38,7 +40,16 @@ object WalletConnectModule {
|
|||
defaultValue = emptySet(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "wallet_connect_sessions") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
scope = scope,
|
||||
),
|
||||
pendingApprovalSessionsStore = DataStoreFactory.create(
|
||||
serializer = MoshiDataStoreSerializer(
|
||||
moshi = moshi,
|
||||
types = setTypes<WcPendingApprovalSessionDTO>(),
|
||||
defaultValue = emptySet(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "wallet_connect_pending_approval_sessions") },
|
||||
scope = scope,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ data class EnvironmentConfig(
|
|||
val mercuryoWidgetId: String = "",
|
||||
val mercuryoSecret: String = "",
|
||||
val amplitudeApiKey: String = "",
|
||||
val appsFlyerApiKey: String = "",
|
||||
val appsAppId: String = "",
|
||||
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
|
||||
val walletConnectProjectId: String = "",
|
||||
val express: ExpressModel? = null,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, E
|
|||
mercuryoSecret = value.mercuryoSecret,
|
||||
blockchainSdkConfig = BlockchainSDKConfigConverter.convert(value = value),
|
||||
amplitudeApiKey = value.amplitudeApiKey,
|
||||
appsFlyerApiKey = value.appsFlyer.appsFlyerDevKey,
|
||||
appsAppId = value.appsFlyer.appsFlyerAppID,
|
||||
walletConnectProjectId = value.walletConnectProjectId,
|
||||
express = value.express,
|
||||
devExpress = value.devExpress,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class EnvironmentConfigModel(
|
|||
@Json(name = "infuraProjectId") val infuraProjectId: String?,
|
||||
@Json(name = "tronGridApiKey") val tronGridApiKey: String,
|
||||
@Json(name = "amplitudeApiKey") val amplitudeApiKey: String,
|
||||
@Json(name = "appsFlyer") val appsFlyer: AppsFlyerModel,
|
||||
@Json(name = "kaspaSecondaryApiUrl") val kaspaSecondaryApiUrl: String,
|
||||
@Json(name = "walletConnectProjectId") val walletConnectProjectId: String,
|
||||
@Json(name = "chiaFireAcademyApiKey") val chiaFireAcademyApiKey: String?,
|
||||
|
|
@ -102,4 +103,12 @@ data class ExpressModel(
|
|||
val apiKey: String,
|
||||
@Json(name = "signVerifierPublicKey")
|
||||
val signVerifierPublicKey: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AppsFlyerModel(
|
||||
@Json(name = "appsFlyerDevKey")
|
||||
val appsFlyerDevKey: String,
|
||||
@Json(name = "appsFlyerAppID")
|
||||
val appsFlyerAppID: String,
|
||||
)
|
||||
|
|
@ -14,11 +14,18 @@ interface RuntimeStateStore<T> {
|
|||
/** Get flow of elements [T] */
|
||||
fun get(): StateFlow<T>
|
||||
|
||||
/** Get element [T] synchronously or null */
|
||||
suspend fun getSyncOrNull(): T?
|
||||
|
||||
/** Store [value] */
|
||||
suspend fun store(value: T)
|
||||
|
||||
/** Update current value by [function] */
|
||||
suspend fun update(function: (T) -> T)
|
||||
|
||||
/** Clear stored value */
|
||||
fun clear()
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
|
|
@ -32,6 +39,8 @@ interface RuntimeStateStore<T> {
|
|||
|
||||
override fun get(): StateFlow<T> = flow
|
||||
|
||||
override suspend fun getSyncOrNull(): T? = flow.value
|
||||
|
||||
override suspend fun store(value: T) {
|
||||
flow.value = value
|
||||
}
|
||||
|
|
@ -39,6 +48,10 @@ interface RuntimeStateStore<T> {
|
|||
override suspend fun update(function: (T) -> T) {
|
||||
flow.update(function)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
flow.value = defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ package com.tangem.datasource.local.network.entity
|
|||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM.NoAccount
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM.Verified
|
||||
import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType
|
||||
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -44,6 +42,7 @@ sealed interface NetworkStatusDM {
|
|||
@Json(name = "selected_address") override val selectedAddress: String,
|
||||
@Json(name = "available_addresses") override val availableAddresses: Set<Address>,
|
||||
@Json(name = "amounts") val amounts: Map<String, BigDecimal>,
|
||||
@Json(name = "yield_supply_statuses") val yieldSupplyStatuses: Map<String, YieldSupplyStatus?> = emptyMap(),
|
||||
) : NetworkStatusDM
|
||||
|
||||
/**
|
||||
|
|
@ -107,4 +106,11 @@ sealed interface NetworkStatusDM {
|
|||
Secondary,
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class YieldSupplyStatus(
|
||||
@Json(name = "is_active") val isActive: Boolean,
|
||||
@Json(name = "is_initialized") val isInitialized: Boolean,
|
||||
@Json(name = "is_allowed_to_spend") val isAllowedToSpend: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TANGEM_TOS_ACC
|
|||
import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_ASK_BIOMETRY_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY
|
||||
|
|
@ -26,7 +26,7 @@ object PreferencesKeys {
|
|||
|
||||
val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") }
|
||||
|
||||
val SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") }
|
||||
val SHOULD_SHOW_ASK_BIOMETRY_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") }
|
||||
|
||||
val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") }
|
||||
|
||||
|
|
@ -56,10 +56,6 @@ object PreferencesKeys {
|
|||
|
||||
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
|
||||
|
||||
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
|
||||
|
||||
val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") }
|
||||
|
||||
val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") }
|
||||
|
||||
val IS_TANGEM_TOS_ACCEPTED_KEY by lazy { booleanPreferencesKey(name = "tangem_tos_accepted") }
|
||||
|
|
@ -187,7 +183,7 @@ object PreferencesKeys {
|
|||
internal fun getTapPrefKeysToMigrate(): Set<String> {
|
||||
return setOf(
|
||||
SAVE_USER_WALLETS_KEY,
|
||||
SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY,
|
||||
SHOULD_SHOW_ASK_BIOMETRY_KEY,
|
||||
APP_LAUNCH_COUNT_KEY,
|
||||
SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY,
|
||||
FUNDS_FOUND_DATE_KEY,
|
||||
|
|
|
|||
|
|
@ -39,5 +39,13 @@ internal class DefaultUserTokensResponseStore(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun clear(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.updateData { preferences ->
|
||||
val key = createPreferencesKey(userWalletId = userWalletId.stringValue)
|
||||
|
||||
preferences.toMutablePreferences().apply { remove(key) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPreferencesKey(userWalletId: String) = stringPreferencesKey(name = "user_tokens_$userWalletId")
|
||||
}
|
||||
|
|
@ -17,4 +17,6 @@ interface UserTokensResponseStore {
|
|||
suspend fun getSyncOrNull(userWalletId: UserWalletId): UserTokensResponse?
|
||||
|
||||
suspend fun store(userWalletId: UserWalletId, response: UserTokensResponse)
|
||||
|
||||
suspend fun clear(userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ interface UserWalletsStore {
|
|||
|
||||
val userWallets: Flow<List<UserWallet>>
|
||||
|
||||
val userWalletsSync: List<UserWallet>
|
||||
|
||||
fun getSyncOrNull(key: UserWalletId): UserWallet?
|
||||
|
||||
fun getSyncStrict(key: UserWalletId): UserWallet
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.domain.visa.model.VisaAuthTokens
|
||||
|
||||
interface TangemPayStorage {
|
||||
|
||||
suspend fun storeAuthTokens(customerWalletAddress: String, tokens: VisaAuthTokens)
|
||||
|
||||
suspend fun getAuthTokens(customerWalletAddress: String): VisaAuthTokens?
|
||||
|
||||
suspend fun storeCustomerWalletAddress(customerWalletAddress: String)
|
||||
|
||||
suspend fun getCustomerWalletAddress(): String?
|
||||
|
||||
suspend fun clear(customerWalletAddress: String)
|
||||
}
|
||||
|
|
@ -1,18 +1,39 @@
|
|||
package com.tangem.datasource.local.walletconnect
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.domain.walletconnect.model.WcPendingApprovalSessionDTO
|
||||
import com.tangem.domain.walletconnect.model.WcSessionDTO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal typealias WcSessionCollection = Set<WcSessionDTO>
|
||||
|
||||
internal class DefaultWalletConnectStore(
|
||||
private val persistenceStore: DataStore<WcSessionCollection>,
|
||||
private val pendingApprovalSessionsStore: DataStore<Set<WcPendingApprovalSessionDTO>>,
|
||||
) : WalletConnectStore {
|
||||
|
||||
override val sessions: Flow<WcSessionCollection>
|
||||
get() = persistenceStore.data
|
||||
|
||||
override val pendingApproval: Flow<Set<WcPendingApprovalSessionDTO>>
|
||||
get() = pendingApprovalSessionsStore.data
|
||||
.transform { pendingApprovalSet ->
|
||||
val now = DateTime.now()
|
||||
val expired = pendingApprovalSet
|
||||
.filterTo(mutableSetOf()) { it.expiredTime < now.millis }
|
||||
val shouldSomeClear = expired.isNotEmpty()
|
||||
val actualData = if (shouldSomeClear) {
|
||||
removePendingApproval(expired)
|
||||
} else {
|
||||
pendingApprovalSet
|
||||
}
|
||||
emit(actualData)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override suspend fun saveSessions(sessions: WcSessionCollection) {
|
||||
persistenceStore.updateData { data -> data.plus(sessions) }
|
||||
}
|
||||
|
|
@ -20,4 +41,16 @@ internal class DefaultWalletConnectStore(
|
|||
override suspend fun removeSessions(sessions: WcSessionCollection) {
|
||||
persistenceStore.updateData { data -> data.minus(sessions) }
|
||||
}
|
||||
|
||||
override suspend fun savePendingApproval(
|
||||
sessions: Set<WcPendingApprovalSessionDTO>,
|
||||
): Set<WcPendingApprovalSessionDTO> {
|
||||
return pendingApprovalSessionsStore.updateData { data -> data.plus(sessions) }
|
||||
}
|
||||
|
||||
override suspend fun removePendingApproval(
|
||||
sessions: Set<WcPendingApprovalSessionDTO>,
|
||||
): Set<WcPendingApprovalSessionDTO> {
|
||||
return pendingApprovalSessionsStore.updateData { data -> data.minus(sessions) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.datasource.local.walletconnect
|
||||
|
||||
import com.tangem.domain.walletconnect.model.WcPendingApprovalSessionDTO
|
||||
import com.tangem.domain.walletconnect.model.WcSessionDTO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -7,6 +8,8 @@ import kotlinx.coroutines.flow.first
|
|||
interface WalletConnectStore {
|
||||
|
||||
val sessions: Flow<WcSessionCollection>
|
||||
val pendingApproval: Flow<Set<WcPendingApprovalSessionDTO>>
|
||||
|
||||
suspend fun findSessionByTopic(topic: String) = sessions.first().find { it.topic == topic }
|
||||
|
||||
suspend fun saveSessions(sessions: WcSessionCollection)
|
||||
|
|
@ -14,4 +17,7 @@ interface WalletConnectStore {
|
|||
|
||||
suspend fun removeSessions(sessions: WcSessionCollection)
|
||||
suspend fun removeSession(session: WcSessionDTO) = removeSessions(setOf(session))
|
||||
|
||||
suspend fun savePendingApproval(sessions: Set<WcPendingApprovalSessionDTO>): Set<WcPendingApprovalSessionDTO>
|
||||
suspend fun removePendingApproval(sessions: Set<WcPendingApprovalSessionDTO>): Set<WcPendingApprovalSessionDTO>
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.datasource.utils
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
|
||||
suspend fun <T> DataStore<T>.getSyncOrNull(): T? = data.firstOrNull()
|
||||
|
|
@ -44,7 +44,11 @@ class NetworkLogsSaveInterceptor(
|
|||
throw e
|
||||
}
|
||||
|
||||
logResponseMessage(response, startNs)
|
||||
if (restrictedForLogURLs.contains(request.url.host + request.url.encodedPath)) {
|
||||
logResponseWithEmptyMessage(response, startNs)
|
||||
} else {
|
||||
logResponseMessage(response, startNs)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
|
@ -87,6 +91,14 @@ class NetworkLogsSaveInterceptor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun logResponseWithEmptyMessage(response: Response, startNs: Long) {
|
||||
val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
|
||||
saveLogMessage(
|
||||
"<-- ${response.code}",
|
||||
" ${response.request.url} (${tookMs}ms)\n",
|
||||
)
|
||||
}
|
||||
|
||||
private fun logResponseMessage(response: Response, startNs: Long) {
|
||||
val responseHeaders = response.headers
|
||||
val responseBody = response.body!!
|
||||
|
|
@ -202,4 +214,13 @@ class NetworkLogsSaveInterceptor(
|
|||
private fun saveLogMessage(vararg messages: String) {
|
||||
appLogsStore.saveLogMessage(tag = "NetworkLogs", *messages)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/**
|
||||
* List of URLs (host + path) for which logging is restricted
|
||||
*/
|
||||
val restrictedForLogURLs = listOf(
|
||||
"api.stakek.it/v1/yields/enabled",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,10 @@ class NetworkStatusDMSerializationTest {
|
|||
{ "value": "0x123456", "type": "primary" },
|
||||
{ "value": "0xabcdef", "type": "secondary" }
|
||||
],
|
||||
"amounts": { "ETH": "1.2345" }
|
||||
"amounts": { "ETH": "1.2345" },
|
||||
"yield_supply_statuses": {
|
||||
"ETH": { "is_active": false, "is_initialized": false, "is_allowed_to_spend": false }
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
|
|
@ -62,6 +65,13 @@ class NetworkStatusDMSerializationTest {
|
|||
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
|
||||
),
|
||||
amounts = mapOf("ETH" to BigDecimal("1.2345")),
|
||||
yieldSupplyStatuses = mapOf(
|
||||
"ETH" to NetworkStatusDM.YieldSupplyStatus(
|
||||
isActive = false,
|
||||
isInitialized = false,
|
||||
isAllowedToSpend = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Truth.assertThat(result).isEqualTo(expected)
|
||||
|
|
@ -82,6 +92,13 @@ class NetworkStatusDMSerializationTest {
|
|||
NetworkStatusDM.Address("0xabcdef", NetworkStatusDM.Address.Type.Secondary),
|
||||
),
|
||||
amounts = mapOf("ETH" to BigDecimal("1.2345")),
|
||||
yieldSupplyStatuses = mapOf(
|
||||
"ETH" to NetworkStatusDM.YieldSupplyStatus(
|
||||
isActive = false,
|
||||
isInitialized = false,
|
||||
isAllowedToSpend = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
|
|
@ -100,7 +117,10 @@ class NetworkStatusDMSerializationTest {
|
|||
{ "value": "0x123456", "type": "primary" },
|
||||
{ "value": "0xabcdef", "type": "secondary" }
|
||||
],
|
||||
"amounts": { "ETH": "1.2345" }
|
||||
"amounts": { "ETH": "1.2345" },
|
||||
"yield_supply_statuses": {
|
||||
"ETH": { "is_active": false, "is_initialized": false, "is_allowed_to_spend": false }
|
||||
}
|
||||
}
|
||||
""".stripJsonWhitespace()
|
||||
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@
|
|||
<string name="common_origin_ring">Primärring</string>
|
||||
<string name="common_passphrase">Passphrase</string>
|
||||
<string name="common_paste">Einfügen</string>
|
||||
<string name="common_privacy_policy">Datenschutzbestimmungen</string>
|
||||
<string name="common_privacy_policy">Datenschutzrichtlinie</string>
|
||||
<string name="common_range">%1$s-%2$s</string>
|
||||
<string name="common_range_with_space">%1$s — %2$s</string>
|
||||
<string name="common_read_more">Weiterlesen</string>
|
||||
|
|
@ -317,6 +317,7 @@
|
|||
<string name="common_supported_networks">Unterstützte Netzwerke</string>
|
||||
<string name="common_swap">Tauschen</string>
|
||||
<string name="common_tangem">Tangem</string>
|
||||
<string name="common_tangem_wallet">Tangem Wallet</string>
|
||||
<string name="common_terms_and_conditions">Allgemeine Geschäftsbedingungen</string>
|
||||
<string name="common_terms_of_use">Nutzungsbedingungen</string>
|
||||
<string name="common_today">Heute</string>
|
||||
|
|
@ -418,7 +419,7 @@
|
|||
<string name="express_exchange_by">Getauscht von %s</string>
|
||||
<string name="express_exchange_notification_failed_text">Besuche die Website des Anbieters, um dein Geld zurückzuerhalten</string>
|
||||
<string name="express_exchange_notification_failed_title">Fehler beim Vorgang durch Anbieter</string>
|
||||
<string name="express_exchange_notification_long_transaction_time_text">Dein Wechsel dauert länger als erwartet. Wende Dich für Hilfe bitte an den Support Deines Anbieters.</string>
|
||||
<string name="express_exchange_notification_long_transaction_time_text">Dein Swap dauert länger als üblich, aber Dein Geld ist absolut sicher und wird geliefert. Bei Fragen kannst Du Dich an das Support-Team des Anbieters wenden.</string>
|
||||
<string name="express_exchange_notification_long_transaction_time_title">Lange Transaktionszeit</string>
|
||||
<string name="express_exchange_notification_refund_text">Der Transaktionsbetrag wurde aufgrund von OKX- oder Bridge-Regeln in %1$s auf deine Wallet zurückerstattet. %2$s</string>
|
||||
<string name="express_exchange_notification_refund_title">Der Betrag wurde in %1$s (%2$s Netzwerk) zurückerstattet.</string>
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@
|
|||
<string name="common_camera_denied_alert_message">カメラへのアクセスを許可していません。プライバシー設定を調整してください。</string>
|
||||
<string name="common_cancel">キャンセル</string>
|
||||
<string name="common_change">変更</string>
|
||||
<string name="common_choose_account">アカウントを選択</string>
|
||||
<string name="common_choose_action">アクションを選択</string>
|
||||
<string name="common_choose_network">ネットワークを選択</string>
|
||||
<string name="common_choose_token">トークンを選択</string>
|
||||
|
|
@ -1247,6 +1248,14 @@
|
|||
<string name="swapping_to_title">受け取る</string>
|
||||
<string name="swapping_token_list_title">トークンを選択</string>
|
||||
<string name="swapping_token_not_available">利用不可</string>
|
||||
<string name="tangempay_onboarding_get_card_button_text">カードをGET</string>
|
||||
<string name="tangempay_onboarding_pay_description">ウォレットに追加して、どこでもスマートフォンで支払いができます。</string>
|
||||
<string name="tangempay_onboarding_pay_title">Apple Pay & Google Pay</string>
|
||||
<string name="tangempay_onboarding_purchases_description">USDC残高を使用して、毎日のお買い物をシームレスに支払います。</string>
|
||||
<string name="tangempay_onboarding_purchases_title">日常の買い物</string>
|
||||
<string name="tangempay_onboarding_security_description">カードの詳細は保護されており、アプリ内で完全に制御できます。</string>
|
||||
<string name="tangempay_onboarding_security_title">内蔵セキュリティ</string>
|
||||
<string name="tangempay_onboarding_title">無料のCryptoカード\nを数分で入手しましょう</string>
|
||||
<string name="this_is_my_wallet_title">これは私のウォレットです</string>
|
||||
<string name="toast_balances_hidden">残高非表示</string>
|
||||
<string name="toast_balances_shown">残高表示</string>
|
||||
|
|
@ -1634,7 +1643,6 @@
|
|||
<string name="yield_module_earn_sheet_current_apy_title">現在のAPY</string>
|
||||
<string name="yield_module_earn_sheet_my_funds_title">私の資金</string>
|
||||
<string name="yield_module_earn_sheet_provider_description">あなたの%1$sはAaveに預けられ、利息が付きます。あなたは%2$sトークンを保有しており、これは残高を表し、時間の経過とともに増加します。入金すると、資金はAaveに預け入れられ、取引手数料を差し引いた利息が付きます。</string>
|
||||
<string name="yield_module_earn_sheet_status_active">アクティブ</string>
|
||||
<string name="yield_module_earn_sheet_title">利回りを得る</string>
|
||||
<string name="yield_module_earn_sheet_total_earnings_title">総収益</string>
|
||||
<string name="yield_module_earn_sheet_transfers_title">Aaveへの送金</string>
|
||||
|
|
@ -1656,8 +1664,8 @@
|
|||
<string name="yield_module_promo_screen_how_it_works_button_title">使い方</string>
|
||||
<string name="yield_module_promo_screen_self_custodial_subtitle">Aaveは世界中で何百万人もの人々に信頼されています。総貸付額は104億ドルです。</string>
|
||||
<string name="yield_module_promo_screen_self_custodial_title">分散型・自己管理型</string>
|
||||
<string name="yield_module_promo_screen_terms_disclaimer">サービスを使用することにより、プロバイダーに同意したものとみなされます</string>
|
||||
<string name="yield_module_promo_screen_title">年間%s%の収益</string>
|
||||
<string name="yield_module_promo_screen_terms_disclaimer">サービスを利用することにより、プロバイダー\n %1$sおよび%2$sに同意したことになります</string>
|
||||
<string name="yield_module_promo_screen_title">年間%s%%の収益</string>
|
||||
<string name="yield_module_promo_screen_variable_rate_info">Aave • 変動金利</string>
|
||||
<string name="yield_module_provider">Aave</string>
|
||||
<string name="yield_module_rate_info_sheet_chart_average">平均%s</string>
|
||||
|
|
@ -1670,6 +1678,7 @@
|
|||
<string name="yield_module_start_earning_sheet_description">%sはAaveに供給され、すぐに利用可能になります</string>
|
||||
<string name="yield_module_start_earning_sheet_fee_policy">手数料ポリシーを見る</string>
|
||||
<string name="yield_module_start_earning_sheet_next_deposits">次回の入金は自動的にAaveに供給されます。</string>
|
||||
<string name="yield_module_status_active">アクティブ</string>
|
||||
<string name="yield_module_stop_earning">収益を停止する</string>
|
||||
<string name="yield_module_stop_earning_sheet_description">オフにすると、Aaveから資金が引き出され、ウォレットの%sに戻され、報酬の獲得が停止されます。</string>
|
||||
<string name="yield_module_stop_earning_sheet_fee_note">出金金額からネットワーク手数料が差し引かれます。</string>
|
||||
|
|
|
|||
|
|
@ -644,6 +644,8 @@
|
|||
<string name="notification_referral_promo_button">Присоединиться</string>
|
||||
<string name="notification_referral_promo_text">Поделись промокодом — заработай 5 USDT с каждой покупки. Твои друзья получат скидку 10% на карту Tangem!</string>
|
||||
<string name="notification_referral_promo_title">Получай бонусы за каждого друга!</string>
|
||||
<string name="notification_sepa_text">Tangem снижает комиссии SEPA почти до нуля, предлагая вам лучшие условия.</string>
|
||||
<string name="notification_sepa_title">Покупайте криптовалюту без комиссии!</string>
|
||||
<string name="onboarding_access_code_feature_1_description">Установите единый код доступа для защиты всех ваших карт или колец</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Защита</string>
|
||||
<string name="onboarding_access_code_feature_2_description">Установите индивидуальный код доступа для каждой карты или кольца позже.</string>
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@
|
|||
<string name="common_camera_denied_alert_message">You have not given access to your camera, please adjust your privacy settings</string>
|
||||
<string name="common_cancel">Cancel</string>
|
||||
<string name="common_change">Change</string>
|
||||
<string name="common_choose_account">Choose account</string>
|
||||
<string name="common_choose_action">Choose action</string>
|
||||
<string name="common_choose_network">Choose network</string>
|
||||
<string name="common_choose_token">Choose token</string>
|
||||
|
|
@ -511,18 +512,18 @@
|
|||
<string name="hot_access_code_set_biometric_ask">Do you want to allow “Tangem” to use biometric authentication? To confirm your identity and open the app</string>
|
||||
<string name="hot_crypto_add_token_subtitle">to %s</string>
|
||||
<string name="hot_crypto_token_network">On %s network</string>
|
||||
<string name="hw_access_code_create_alert_title">Are you sure you want to exit the access code creation process?</string>
|
||||
<string name="hw_access_code_create_alert_title">Are you sure you want to cancel access code setup?</string>
|
||||
<string name="hw_activation_need_backup">Backup Now</string>
|
||||
<string name="hw_activation_need_description">To complete setup, back up your wallet and secure app access with a Access Code.</string>
|
||||
<string name="hw_activation_need_finish">Finish Now</string>
|
||||
<string name="hw_activation_need_title">Finish Wallet Activation</string>
|
||||
<string name="hw_activation_need_warning_description">To complete setup, secure app access with Access Code.</string>
|
||||
<string name="hw_activation_need_warning_description">Complete setup by securing the app with an access code.</string>
|
||||
<string name="hw_backup_alert_description">If you do, you\'ll need to start over.</string>
|
||||
<string name="hw_backup_alert_title">Are you sure you want to exit the activation process?</string>
|
||||
<string name="hw_backup_alert_title">Are you sure you want to quit the activation process?</string>
|
||||
<string name="hw_backup_close_description">If you do, you\'ll need to start over.</string>
|
||||
<string name="hw_backup_google_drive_description">Recover an existing wallet stored in your Google Drive backup</string>
|
||||
<string name="hw_backup_google_drive_title">Google Drive Backup</string>
|
||||
<string name="hw_backup_hardware_description">Upgrade your security right away with a best in class hardware wallet from Tangem.</string>
|
||||
<string name="hw_backup_google_drive_description">Recover existing wallet via Google Drive backup</string>
|
||||
<string name="hw_backup_google_drive_title">Google Drive backup</string>
|
||||
<string name="hw_backup_hardware_description">Level up your security with a superior Tangem hardware wallet.</string>
|
||||
<string name="hw_backup_hardware_title">Hardware Wallet</string>
|
||||
<string name="hw_backup_need_action">Go to backup</string>
|
||||
<string name="hw_backup_need_description">To secure your wallet with a Access Code, complete the backup first.</string>
|
||||
|
|
@ -537,22 +538,22 @@
|
|||
<string name="hw_create_title">Create Mobile Wallet</string>
|
||||
<string name="hw_import_seed_phrase_already_imported">This recovery phrase has already been imported</string>
|
||||
<string name="hw_mobile_wallet">Mobile Wallet</string>
|
||||
<string name="hw_upgrade_error_card_already_has_wallet">This device can’t be used for upgrade, it already contains another wallet.</string>
|
||||
<string name="hw_upgrade_error_card_key_import">Choose another device, this one can’t be used for upgrade.</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>
|
||||
<string name="hw_upgrade_error_wallet2_card_required">An error occurred during the operation.</string>
|
||||
<string name="hw_upgrade_funds_access_description">Your funds stay safe and fully accessible during the process</string>
|
||||
<string name="hw_upgrade_funds_access_title">Funds access</string>
|
||||
<string name="hw_upgrade_general_security_description">All private wallet data will be removed from the mobile app and stored securely on your Tangem device only</string>
|
||||
<string name="hw_upgrade_general_security_description">Your wallet data will be erased from the app and stored on your Tangem device</string>
|
||||
<string name="hw_upgrade_general_security_title">General Security</string>
|
||||
<string name="hw_upgrade_key_migration_description">Private keys will be moved from the app to your Tangem card or ring</string>
|
||||
<string name="hw_upgrade_key_migration_description">Private keys will be moved from the app to your Tangem device</string>
|
||||
<string name="hw_upgrade_key_migration_title">Key Migration</string>
|
||||
<string name="hw_upgrade_scan_device">Scan device</string>
|
||||
<string name="hw_upgrade_start_action">Start upgrade</string>
|
||||
<string name="hw_upgrade_start_description">You’re about to upgrade your wallet to Tangem Wallet. This will keep your assets safe with cold storage.</string>
|
||||
<string name="hw_upgrade_start_description">You’re about to upgrade to a Tangem device, where your assets stay safe in cold storage.</string>
|
||||
<string name="hw_upgrade_start_title">Tangem Wallet</string>
|
||||
<string name="hw_upgrade_title">Upgrade to Hardware Wallet</string>
|
||||
<string name="hw_upgrade_to_cold_banner_description">Keep your crypto safe with Tangem’s best-in-class hardware wallet.</string>
|
||||
<string name="hw_upgrade_to_cold_banner_title">Upgrade wallet with a hardware backup</string>
|
||||
<string name="hw_upgrade_to_cold_banner_description">Keep your crypto safe with Tangem\'s top-tier hardware wallet.</string>
|
||||
<string name="hw_upgrade_to_cold_banner_title">Upgrade your wallet to hardware storage</string>
|
||||
<string name="information_generated_with_ai">This information was generated with AI.\nTap here, if you find any errors.</string>
|
||||
<string name="initial_message_change_access_code_body">To change the access code tap the card or ring as shown above and do not remove until the end of the operation</string>
|
||||
<string name="initial_message_change_passcode_body">To change the passcode tap the card as shown above and do not remove until the end of the operation</string>
|
||||
|
|
@ -758,8 +759,8 @@
|
|||
<string name="notification_referral_promo_text">Share your code - earn 5 USDT per sale. Your friend gets 10% OFF.</string>
|
||||
<string name="notification_referral_promo_title">Get REWARDS for every friend!</string>
|
||||
<string name="notification_sepa_button">Buy crypto</string>
|
||||
<string name="notification_sepa_text">Enjoy zero fees when purchasing crypto via SEPA transfer.</string>
|
||||
<string name="notification_sepa_title">Buy Crypto with SEPA</string>
|
||||
<string name="notification_sepa_text">Tangem slashes SEPA fees to almost nothing, giving you the best deal yet.</string>
|
||||
<string name="notification_sepa_title">Buy crypto and don\'t pay fees!</string>
|
||||
<string name="onboarding_access_code_feature_1_description">Set up a single access code to protect all your devices.</string>
|
||||
<string name="onboarding_access_code_feature_1_title">Protect</string>
|
||||
<string name="onboarding_access_code_feature_2_description">Set an individual access code for each card or ring later.</string>
|
||||
|
|
@ -1266,6 +1267,14 @@
|
|||
<string name="swapping_to_title">You receive</string>
|
||||
<string name="swapping_token_list_title">Choose token</string>
|
||||
<string name="swapping_token_not_available">not available</string>
|
||||
<string name="tangempay_onboarding_get_card_button_text">Get card</string>
|
||||
<string name="tangempay_onboarding_pay_description">Add to your wallet and pay with your phone anywhere.</string>
|
||||
<string name="tangempay_onboarding_pay_title">Apple Pay & Google Pay</string>
|
||||
<string name="tangempay_onboarding_purchases_description">Use your USDC balance to pay for everyday purchases seamlessly.</string>
|
||||
<string name="tangempay_onboarding_purchases_title">Everyday purchases</string>
|
||||
<string name="tangempay_onboarding_security_description">Your card details are protected — full control in the app.</string>
|
||||
<string name="tangempay_onboarding_security_title">Built-in security</string>
|
||||
<string name="tangempay_onboarding_title">Get your free Crypto Card\nin minutes</string>
|
||||
<string name="this_is_my_wallet_title">This is my wallet</string>
|
||||
<string name="toast_balances_hidden">Balances hidden</string>
|
||||
<string name="toast_balances_shown">Balances shown</string>
|
||||
|
|
@ -1614,6 +1623,7 @@
|
|||
<string name="wc_alert_wrong_card_title">We\'ve got some kind of problem</string>
|
||||
<string name="wc_all_dapps_disconnected">All dApps disconnected</string>
|
||||
<string name="wc_allow_to_spend">Allow to spend</string>
|
||||
<string name="wc_approve_description">By approving, you allow dApp or Smart contract to use tokens in future transactions.</string>
|
||||
<string name="wc_common_address">Address</string>
|
||||
<string name="wc_common_connect">Connect</string>
|
||||
<string name="wc_common_loading">Loading</string>
|
||||
|
|
@ -1702,7 +1712,6 @@
|
|||
<string name="yield_module_earn_sheet_current_apy_title">Current APY</string>
|
||||
<string name="yield_module_earn_sheet_my_funds_title">My Funds</string>
|
||||
<string name="yield_module_earn_sheet_provider_description">Your %1$s is now deposited in Aave and earning interest. You hold a%2$s token, which represents your balance and grows over time. When you top up, funds go to Aave to earn interest, minus a transaction fee.</string>
|
||||
<string name="yield_module_earn_sheet_status_active">Active</string>
|
||||
<string name="yield_module_earn_sheet_title">Earn</string>
|
||||
<string name="yield_module_earn_sheet_total_earnings_title">Total earnings</string>
|
||||
<string name="yield_module_earn_sheet_transfers_title">Transfers to Aave</string>
|
||||
|
|
@ -1724,8 +1733,8 @@
|
|||
<string name="yield_module_promo_screen_how_it_works_button_title">How it works?</string>
|
||||
<string name="yield_module_promo_screen_self_custodial_subtitle">Aave is trusted by millions worldwide. Total lended value is $10.4B.</string>
|
||||
<string name="yield_module_promo_screen_self_custodial_title">Decentralized and self-custodial</string>
|
||||
<string name="yield_module_promo_screen_terms_disclaimer">By using service, you agree with provider</string>
|
||||
<string name="yield_module_promo_screen_title">Earn %s% yearly</string>
|
||||
<string name="yield_module_promo_screen_terms_disclaimer">By using service, you agree with provider\n%1$s and %2$s</string>
|
||||
<string name="yield_module_promo_screen_title">Earn %s%% yearly</string>
|
||||
<string name="yield_module_promo_screen_variable_rate_info">Aave • Variable Interest Rate</string>
|
||||
<string name="yield_module_provider">Aave</string>
|
||||
<string name="yield_module_rate_info_sheet_chart_average">Avg %s</string>
|
||||
|
|
@ -1738,6 +1747,8 @@
|
|||
<string name="yield_module_start_earning_sheet_description">Your %s will be supplied to Aave and will stay instantly available</string>
|
||||
<string name="yield_module_start_earning_sheet_fee_policy">See fee policy</string>
|
||||
<string name="yield_module_start_earning_sheet_next_deposits">Your next deposits will be automatically supplied to Aave.</string>
|
||||
<string name="yield_module_status_active">Active</string>
|
||||
<string name="yield_module_status_paused">Paused</string>
|
||||
<string name="yield_module_stop_earning">Stop earning</string>
|
||||
<string name="yield_module_stop_earning_sheet_description">Turning off will withdraw your funds from Aave, return them to %s in your wallet, and stop earning rewards.</string>
|
||||
<string name="yield_module_stop_earning_sheet_fee_note">The network fee will be deducted from the amount you withdraw.</string>
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ fun PrimaryButton(
|
|||
@Composable
|
||||
fun PrimaryButtonIconEnd(
|
||||
text: String,
|
||||
@DrawableRes iconResId: Int,
|
||||
@DrawableRes iconResId: Int?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
size: TangemButtonSize = TangemButtonSize.Default,
|
||||
|
|
@ -122,7 +122,7 @@ fun PrimaryButtonIconEnd(
|
|||
TangemButton(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
icon = TangemButtonIconPosition.End(iconResId),
|
||||
icon = if (iconResId != null) TangemButtonIconPosition.End(iconResId) else TangemButtonIconPosition.None,
|
||||
onClick = onClick,
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
enabled = enabled,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import com.tangem.core.ui.components.fields.SimpleDialogTextField
|
|||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.DialogTestTags
|
||||
import com.tangem.core.ui.test.BaseDialogTestTags
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
|
@ -219,13 +219,14 @@ private fun TangemDialog(
|
|||
color = TangemTheme.colors.background.primary,
|
||||
)
|
||||
.padding(vertical = TangemTheme.dimens.spacing24)
|
||||
.testTag(DialogTestTags.DIALOG_CONTAINER),
|
||||
.testTag(BaseDialogTestTags.CONTAINER),
|
||||
) {
|
||||
if (title != null) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing24)
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.testTag(BaseDialogTestTags.TITLE),
|
||||
text = title,
|
||||
style = when (type) {
|
||||
is DialogType.Message -> TangemTheme.typography.h2
|
||||
|
|
@ -262,7 +263,8 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) {
|
|||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing24)
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.testTag(BaseDialogTestTags.TEXT),
|
||||
text = type.message,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.core.ui.components
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.Build
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
|
|
@ -100,8 +101,12 @@ private class FullScreenLayout(
|
|||
width = WindowManager.LayoutParams.MATCH_PARENT
|
||||
height = WindowManager.LayoutParams.MATCH_PARENT
|
||||
format = PixelFormat.TRANSLUCENT
|
||||
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
||||
|
||||
if (focusable && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
} else {
|
||||
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
||||
}
|
||||
}
|
||||
|
||||
fun show() {
|
||||
|
|
@ -142,7 +147,7 @@ private class FullScreenLayout(
|
|||
|
||||
fun dispose() {
|
||||
dismiss()
|
||||
setViewTreeSavedStateRegistryOwner(null)
|
||||
setViewTreeLifecycleOwner(null)
|
||||
setViewTreeSavedStateRegistryOwner(null)
|
||||
setViewTreeViewModelStoreOwner(null)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||
|
||||
/**
|
||||
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=281-248&mode=design&t=bXqehWPHyATKcZEW-4)
|
||||
|
|
@ -38,7 +40,7 @@ fun SimpleSettingsRow(
|
|||
onItemsClick()
|
||||
}
|
||||
},
|
||||
),
|
||||
).testTag(BaseBottomSheetTestTags.ACTION_TITLE),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -15,10 +15,12 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.MarketsTestTags
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
|
|
@ -56,7 +58,7 @@ fun TangemSwitch(
|
|||
onClick = {
|
||||
onCheckedChange(!checked)
|
||||
},
|
||||
),
|
||||
).testTag(MarketsTestTags.ADD_TO_PORTFOLIO_SWITCH),
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
package com.tangem.core.ui.components.account
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
enum class AccountIconSize {
|
||||
Default, Large, Medium, Small, ExtraSmall
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a portfolio icon using a predefined vector resource.
|
||||
*
|
||||
* The background color is defined by [color], and the icon tint is constant white.
|
||||
* The icon size and container shape are adapted based on the provided [size].
|
||||
*
|
||||
* @param resId The vector drawable resource ID to display.
|
||||
* @param color The background color of the icon container.
|
||||
* @param size The size of the icon, defined by [AccountIconSize].
|
||||
*/
|
||||
@Composable
|
||||
fun AccountResIcon(@DrawableRes resId: Int, color: Color, size: AccountIconSize, modifier: Modifier = Modifier) {
|
||||
val boxModifier = modifier.selectBoxModifier(size)
|
||||
val iconSize = Modifier.selectIconSize(size)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = boxModifier.background(color),
|
||||
) {
|
||||
Icon(
|
||||
modifier = iconSize,
|
||||
tint = TangemTheme.colors.text.constantWhite,
|
||||
imageVector = ImageVector.vectorResource(id = resId),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a portfolio icon using a single character.
|
||||
*
|
||||
* The background color is defined by [color], the text is uppercased,
|
||||
* and its style is chosen based on the provided [size].
|
||||
* The container shape and size also adapt to [size].
|
||||
*
|
||||
* @param char The character to display inside the icon.
|
||||
* @param color The background color of the icon container.
|
||||
* @param size The size of the icon, defined by [AccountIconSize].
|
||||
*/
|
||||
@Composable
|
||||
fun AccountCharIcon(char: Char, color: Color, size: AccountIconSize, modifier: Modifier = Modifier) {
|
||||
val boxModifier = modifier.selectBoxModifier(size)
|
||||
val textStyle = when (size) {
|
||||
AccountIconSize.Default -> TangemTheme.typography.h3
|
||||
AccountIconSize.Large -> TangemTheme.typography.h1
|
||||
AccountIconSize.Medium -> TangemTheme.typography.subtitle1
|
||||
AccountIconSize.Small -> TangemTheme.typography.subtitle2
|
||||
AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1
|
||||
}
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = boxModifier.background(color),
|
||||
) {
|
||||
Text(
|
||||
text = char.uppercase(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.constantWhite,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.selectIconSize(size: AccountIconSize): Modifier = when (size) {
|
||||
AccountIconSize.Default -> this.size(20.dp)
|
||||
AccountIconSize.Large -> this.size(40.dp)
|
||||
AccountIconSize.Medium -> this.size(16.dp)
|
||||
AccountIconSize.Small -> this.size(12.dp)
|
||||
AccountIconSize.ExtraSmall -> this.size(8.dp)
|
||||
}
|
||||
|
||||
private fun Modifier.selectBoxModifier(size: AccountIconSize): Modifier = when (size) {
|
||||
AccountIconSize.Default -> size(36.dp).clip(RoundedCornerShape(10.dp))
|
||||
AccountIconSize.Large -> size(88.dp).clip(RoundedCornerShape(24.dp))
|
||||
AccountIconSize.Medium -> size(28.dp).clip(RoundedCornerShape(8.dp))
|
||||
AccountIconSize.Small -> size(20.dp).clip(RoundedCornerShape(6.dp))
|
||||
AccountIconSize.ExtraSmall -> size(14.dp).clip(RoundedCornerShape(4.dp))
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
Row(
|
||||
modifier = Modifier.background(TangemTheme.colors.background.primary),
|
||||
) {
|
||||
Sample()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Sample() {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
|
||||
AccountResIcon(resId = R.drawable.ic_shirt_24, color = Color.Red, size = AccountIconSize.Default)
|
||||
AccountResIcon(resId = R.drawable.ic_rounded_star_24, color = Color.Blue, size = AccountIconSize.Large)
|
||||
AccountResIcon(resId = R.drawable.ic_user_24, color = Color.Magenta, size = AccountIconSize.Medium)
|
||||
AccountResIcon(resId = R.drawable.ic_family_24, color = Color.DarkGray, size = AccountIconSize.Small)
|
||||
AccountResIcon(resId = R.drawable.ic_wallet_24, color = Color.Green, size = AccountIconSize.ExtraSmall)
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
|
||||
AccountCharIcon(char = 'D', color = Color.Red, size = AccountIconSize.Default)
|
||||
AccountCharIcon(char = 'L', color = Color.Blue, size = AccountIconSize.Large)
|
||||
AccountCharIcon(char = 'M', color = Color.Magenta, size = AccountIconSize.Medium)
|
||||
AccountCharIcon(char = 'S', color = Color.DarkGray, size = AccountIconSize.Small)
|
||||
AccountCharIcon(char = 'E', color = Color.Green, size = AccountIconSize.ExtraSmall)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,12 @@ sealed class TopAppBarButtonUM(
|
|||
enabled = enabled,
|
||||
)
|
||||
|
||||
fun Close(enabled: Boolean = true, onCloseClick: () -> Unit) = Icon(
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
onClicked = onCloseClick,
|
||||
enabled = enabled,
|
||||
)
|
||||
|
||||
fun Text(text: TextReference, onTextClicked: () -> Unit, enabled: Boolean = true) = Text(
|
||||
text = text,
|
||||
onClicked = onTextClicked,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheet(
|
|||
config: TangemBottomSheetConfig,
|
||||
containerColor: Color = TangemTheme.colors.background.primary,
|
||||
skipPartiallyExpanded: Boolean = true,
|
||||
dismissOnClickOutside: Boolean = true,
|
||||
scrollableContent: Boolean = true,
|
||||
noinline onBack: (() -> Unit)? = null,
|
||||
crossinline title: @Composable BoxScope.(T) -> Unit = {},
|
||||
crossinline content: @Composable ColumnScope.(T) -> Unit,
|
||||
|
|
@ -61,6 +63,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheet(
|
|||
containerColor = containerColor,
|
||||
title = title,
|
||||
content = content,
|
||||
scrollableContent = scrollableContent,
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -69,6 +72,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheet(
|
|||
containerColor = containerColor,
|
||||
title = title,
|
||||
content = content,
|
||||
dismissOnClickOutside = dismissOnClickOutside,
|
||||
scrollableContent = scrollableContent,
|
||||
onBack = onBack,
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
)
|
||||
|
|
@ -81,21 +86,38 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheet(
|
|||
config: TangemBottomSheetConfig,
|
||||
containerColor: Color,
|
||||
skipPartiallyExpanded: Boolean = true,
|
||||
dismissOnClickOutside: Boolean = true,
|
||||
scrollableContent: Boolean = true,
|
||||
noinline onBack: (() -> Unit)? = null,
|
||||
crossinline title: @Composable (BoxScope.(T) -> Unit),
|
||||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(value = config.isShown) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
confirmValueChange = {
|
||||
if (!dismissOnClickOutside) {
|
||||
it != SheetValue.Hidden // Ignore transitions to hidden (prevents dismiss on outside click/back press)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (isVisible && config.content is T) {
|
||||
BasicModalBottomSheet<T>(
|
||||
config = config,
|
||||
sheetState = sheetState,
|
||||
containerColor = containerColor,
|
||||
title = title,
|
||||
onBack = onBack,
|
||||
content = content,
|
||||
bsContent = {
|
||||
BsContent(
|
||||
config = config,
|
||||
containerColor = containerColor,
|
||||
scrollableContent = scrollableContent,
|
||||
title = title,
|
||||
content = content,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +136,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheet(
|
|||
config: TangemBottomSheetConfig,
|
||||
containerColor: Color,
|
||||
skipPartiallyExpanded: Boolean = true,
|
||||
scrollableContent: Boolean = true,
|
||||
crossinline title: @Composable (BoxScope.(T) -> Unit),
|
||||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
) {
|
||||
|
|
@ -125,49 +148,64 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheet(
|
|||
density = LocalDensity.current,
|
||||
),
|
||||
onBack = null,
|
||||
containerColor = containerColor,
|
||||
title = title,
|
||||
content = content,
|
||||
bsContent = {
|
||||
BsContent(
|
||||
config = config,
|
||||
containerColor = containerColor,
|
||||
scrollableContent = scrollableContent,
|
||||
title = title,
|
||||
content = content,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
inline fun <reified T : TangemBottomSheetConfigContent> BsContent(
|
||||
config: TangemBottomSheetConfig,
|
||||
containerColor: Color,
|
||||
scrollableContent: Boolean = true,
|
||||
crossinline title: @Composable (BoxScope.(T) -> Unit),
|
||||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
) {
|
||||
val model = config.content as? T ?: return
|
||||
|
||||
val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.systemBarsPadding()
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersLarge)
|
||||
.background(containerColor)
|
||||
.heightIn(max = maxHeight.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
title(model)
|
||||
}
|
||||
if (scrollableContent) {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
content(model)
|
||||
}
|
||||
} else {
|
||||
content(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
|
||||
config: TangemBottomSheetConfig,
|
||||
sheetState: SheetState,
|
||||
containerColor: Color,
|
||||
noinline onBack: (() -> Unit)? = null,
|
||||
crossinline title: @Composable (BoxScope.(T) -> Unit),
|
||||
crossinline content: @Composable (ColumnScope.(T) -> Unit),
|
||||
noinline bsContent: @Composable ColumnScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val model = config.content as? T ?: return
|
||||
|
||||
val bsContent: @Composable ColumnScope.() -> Unit = {
|
||||
val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.systemBarsPadding()
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
.clip(TangemTheme.shapes.roundedCornersLarge)
|
||||
.background(containerColor)
|
||||
.heightIn(max = maxHeight.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
title(model)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
content(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onBack != null) {
|
||||
ModalBottomSheetWithBackHandling(
|
||||
modifier = modifier,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
|
@ -20,6 +21,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.WalletConnectDetailsBottomSheetTestTags
|
||||
|
||||
/**
|
||||
* Title component for [TangemModalBottomSheet] with [TangemIconButton] for buttons.
|
||||
|
|
@ -54,7 +56,9 @@ fun TangemModalBottomSheetTitle(
|
|||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.testTag(WalletConnectDetailsBottomSheetTestTags.TITLE),
|
||||
)
|
||||
}
|
||||
if (subtitle != null) {
|
||||
|
|
@ -62,7 +66,9 @@ fun TangemModalBottomSheetTitle(
|
|||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.testTag(WalletConnectDetailsBottomSheetTestTags.DATE),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -72,7 +78,8 @@ fun TangemModalBottomSheetTitle(
|
|||
onClick = onEndClick,
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.align(Alignment.CenterEnd),
|
||||
.align(Alignment.CenterEnd)
|
||||
.testTag(WalletConnectDetailsBottomSheetTestTags.CLOSE_BUTTON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.modal
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
|
@ -49,7 +50,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheetWi
|
|||
noinline onBack: (() -> Unit)? = null,
|
||||
crossinline title: @Composable BoxScope.(T) -> Unit = {},
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
crossinline footer: @Composable (BoxScope.(T) -> Unit),
|
||||
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
|
||||
) {
|
||||
val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheetW
|
|||
noinline onBack: (() -> Unit)? = null,
|
||||
crossinline title: @Composable BoxScope.(T) -> Unit,
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
crossinline footer: @Composable (BoxScope.(T) -> Unit),
|
||||
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(value = config.isShown) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
|
||||
|
|
@ -118,7 +119,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
|
|||
skipPartiallyExpanded: Boolean = true,
|
||||
crossinline title: @Composable BoxScope.(T) -> Unit,
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
crossinline footer: @Composable BoxScope.(T) -> Unit,
|
||||
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
|
||||
) {
|
||||
BasicModalBottomSheetWithFooter<T>(
|
||||
config = config,
|
||||
|
|
@ -145,7 +146,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
|
|||
noinline onBack: (() -> Unit)? = null,
|
||||
crossinline title: @Composable BoxScope.(T) -> Unit,
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
crossinline footer: @Composable (BoxScope.(T) -> Unit),
|
||||
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val model = config.content as? T ?: return
|
||||
|
|
@ -156,8 +157,13 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
|
|||
val scrollState = rememberScrollState(initial = initial)
|
||||
|
||||
val isKeyboardOpen by rememberIsKeyboardVisible()
|
||||
val buttonHeight = TangemTheme.dimens.spacing80
|
||||
val contentBottomPadding = TangemTheme.dimens.spacing80
|
||||
val buttonHeight by animateDpAsState(
|
||||
if (footer != null) {
|
||||
80.dp
|
||||
} else {
|
||||
0.dp
|
||||
},
|
||||
)
|
||||
// Offset calculation for keyboard scroll adjustment:
|
||||
// 1) Button height (footer)
|
||||
// 2) Column content bottom padding
|
||||
|
|
@ -202,7 +208,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(state = scrollState)
|
||||
.padding(bottom = contentBottomPadding),
|
||||
.padding(bottom = buttonHeight),
|
||||
) {
|
||||
content(model)
|
||||
}
|
||||
|
|
@ -218,7 +224,9 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
|
|||
.height(buttonHeight)
|
||||
.align(Alignment.BottomCenter),
|
||||
) {
|
||||
footer(model)
|
||||
if (footer != null) {
|
||||
footer(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ package com.tangem.core.ui.components.containers
|
|||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
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
|
||||
|
|
@ -17,14 +18,14 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
*
|
||||
* @param modifier of component
|
||||
* @param footer text
|
||||
* @param footerTopPadding padding between footer and field
|
||||
* @param paddingValues padding between footer and field
|
||||
* @param content field content
|
||||
*/
|
||||
@Composable
|
||||
fun FooterContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
footer: TextReference? = null,
|
||||
footerTopPadding: Dp = TangemTheme.dimens.spacing8,
|
||||
paddingValues: PaddingValues = PaddingValues(top = 8.dp),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
|
|
@ -36,7 +37,7 @@ fun FooterContainer(
|
|||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = footerTopPadding),
|
||||
.padding(paddingValues),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.account.AccountCharIcon
|
||||
import com.tangem.core.ui.components.account.AccountIconSize
|
||||
import com.tangem.core.ui.components.account.AccountResIcon
|
||||
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
|
|
@ -58,6 +62,18 @@ internal fun ContentIcon(
|
|||
background = icon.background,
|
||||
alpha = alpha,
|
||||
)
|
||||
is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon(
|
||||
modifier = modifier,
|
||||
resId = icon.resId,
|
||||
color = icon.color,
|
||||
size = AccountIconSize.Default,
|
||||
)
|
||||
is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon(
|
||||
modifier = modifier,
|
||||
char = icon.char.resolveReference().first(),
|
||||
color = icon.color,
|
||||
size = AccountIconSize.Default,
|
||||
)
|
||||
CurrencyIconState.Loading,
|
||||
CurrencyIconState.Locked,
|
||||
is CurrencyIconState.Empty,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ fun CurrencyIcon(
|
|||
is CurrencyIconState.FiatIcon,
|
||||
is CurrencyIconState.CustomTokenIcon,
|
||||
is CurrencyIconState.TokenIcon,
|
||||
is CurrencyIconState.CryptoPortfolio.Icon,
|
||||
is CurrencyIconState.CryptoPortfolio.Letter,
|
||||
-> {
|
||||
ContentIconContainer(
|
||||
icon = state,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
* Represents the various states an icon can be in.
|
||||
|
|
@ -86,6 +87,25 @@ sealed class CurrencyIconState {
|
|||
override val topBadgeIconResId: Int? = null
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class CryptoPortfolio : CurrencyIconState() {
|
||||
override val showCustomBadge: Boolean = false
|
||||
override val topBadgeIconResId: Int? = null
|
||||
abstract val color: Color
|
||||
|
||||
data class Icon(
|
||||
@DrawableRes val resId: Int,
|
||||
override val color: Color,
|
||||
override val isGrayscale: Boolean,
|
||||
) : CryptoPortfolio()
|
||||
|
||||
data class Letter(
|
||||
val char: TextReference,
|
||||
override val color: Color,
|
||||
override val isGrayscale: Boolean,
|
||||
) : CryptoPortfolio()
|
||||
}
|
||||
|
||||
data object Loading : CurrencyIconState() {
|
||||
override val isGrayscale: Boolean = false
|
||||
override val showCustomBadge: Boolean = false
|
||||
|
|
@ -125,6 +145,12 @@ sealed class CurrencyIconState {
|
|||
showCustomBadge = showCustomBadge,
|
||||
topBadgeIconResId = topBadgeIconResId,
|
||||
)
|
||||
is CryptoPortfolio.Icon -> copy(
|
||||
isGrayscale = isGrayscale,
|
||||
)
|
||||
is CryptoPortfolio.Letter -> copy(
|
||||
isGrayscale = isGrayscale,
|
||||
)
|
||||
is Loading,
|
||||
is Locked,
|
||||
is Empty,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.StakingSendScreenTestTags
|
||||
import com.tangem.core.ui.test.SendScreenTestTags
|
||||
import com.tangem.core.ui.utils.*
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
|
|
@ -106,7 +106,7 @@ fun AmountTextField(
|
|||
visualTransformation = visualTransformation,
|
||||
modifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.testTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD),
|
||||
.testTag(SendScreenTestTags.INPUT_TEXT_FIELD),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import androidx.compose.ui.text.rememberTextMeasurer
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.utils.StringsSigns.PASSWORD_VISUAL_CHAR
|
||||
|
|
@ -159,14 +158,17 @@ private fun CellDecoration(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
Box(
|
||||
modifier = Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight),
|
||||
text = text,
|
||||
style = TangemTheme.typography.h3,
|
||||
color = color,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 48.sp,
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = text,
|
||||
style = TangemTheme.typography.h3,
|
||||
color = color,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -39,7 +38,7 @@ import com.tangem.core.ui.extensions.resolveReference
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags
|
||||
import com.tangem.core.ui.test.BaseSearchBarTestTags
|
||||
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
|
|
@ -47,10 +46,10 @@ fun SearchBar(
|
|||
modifier: Modifier = Modifier,
|
||||
colors: TextFieldColors = TangemSearchBarDefaults.defaultTextFieldColors,
|
||||
enabled: Boolean = true,
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
BasicTextField(
|
||||
|
|
@ -65,7 +64,7 @@ fun SearchBar(
|
|||
}
|
||||
}
|
||||
.focusRequester(focusRequester)
|
||||
.testTag(SelectCountryBottomSheetTestTags.SEARCH_BAR),
|
||||
.testTag(BaseSearchBarTestTags.SEARCH_BAR),
|
||||
enabled = enabled,
|
||||
value = state.query,
|
||||
onValueChange = state.onQueryChange,
|
||||
|
|
@ -97,10 +96,6 @@ fun SearchBar(
|
|||
)
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.core.ui.components.grid
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* EnumeratedTwoColumnGrid component
|
||||
*
|
||||
* @param items component items
|
||||
* @param modifier composable modifier
|
||||
*
|
||||
*/
|
||||
@Composable
|
||||
fun EnumeratedTwoColumnGrid(items: ImmutableList<EnumeratedTwoColumnGridItem>, modifier: Modifier = Modifier) {
|
||||
VerticalGrid(
|
||||
modifier = modifier,
|
||||
items = items,
|
||||
) { item ->
|
||||
Row(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.size8),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (LocalLayoutDirection.current == LayoutDirection.Ltr) {
|
||||
Text(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size40),
|
||||
text = "${item.index}.",
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Text(
|
||||
text = item.mnemonic,
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = item.mnemonic,
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size40),
|
||||
text = "${item.index}.",
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun <T> VerticalGrid(
|
||||
items: ImmutableList<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val columnLength = items.size / 2
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
repeat(2) { index ->
|
||||
Column {
|
||||
for (i in 0 until columnLength) {
|
||||
val item = items[index * columnLength + i]
|
||||
content(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, heightDp = 640, showBackground = true)
|
||||
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
EnumeratedTwoColumnGrid(
|
||||
items = List(24) {
|
||||
EnumeratedTwoColumnGridItem(
|
||||
index = it + 1,
|
||||
mnemonic = "word${it + 1}",
|
||||
)
|
||||
}.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.ui.components.grid.entity
|
||||
|
||||
data class EnumeratedTwoColumnGridItem(
|
||||
val index: Int,
|
||||
val mnemonic: String,
|
||||
)
|
||||
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment.Companion.CenterEnd
|
|||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
|
@ -32,6 +33,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.SendAddressScreenTestTags
|
||||
import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
|
@ -92,6 +94,7 @@ fun InputRowRecipient(
|
|||
text = it.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = color,
|
||||
modifier = Modifier.testTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD_TITLE),
|
||||
)
|
||||
}
|
||||
Box(
|
||||
|
|
@ -115,7 +118,8 @@ fun InputRowRecipient(
|
|||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing12)
|
||||
.weight(1f)
|
||||
.align(CenterVertically),
|
||||
.align(CenterVertically)
|
||||
.testTag(SendAddressScreenTestTags.ADDRESS_TEXT_FIELD),
|
||||
)
|
||||
CrossIcon(
|
||||
onClick = onPasteClick,
|
||||
|
|
|
|||
|
|
@ -2,19 +2,22 @@ package com.tangem.core.ui.components.label
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.label.entity.LabelStyle
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -48,8 +51,18 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
|||
},
|
||||
)
|
||||
|
||||
val iconColor by animateColorAsState(
|
||||
targetValue = when (state.style) {
|
||||
LabelStyle.ACCENT -> TangemTheme.colors.icon.accent
|
||||
LabelStyle.REGULAR -> TangemTheme.colors.icon.informative
|
||||
LabelStyle.WARNING -> TangemTheme.colors.icon.warning
|
||||
},
|
||||
)
|
||||
|
||||
AnimatedContent(targetState = state.text) { text ->
|
||||
Box(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.background(
|
||||
|
|
@ -63,6 +76,15 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
|||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -73,6 +95,7 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
|
|||
private fun LabelPreview() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Label(
|
||||
|
|
@ -81,24 +104,39 @@ private fun LabelPreview() {
|
|||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Regular Label"),
|
||||
style = LabelStyle.REGULAR,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Accent Label"),
|
||||
style = LabelStyle.ACCENT,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
Label(
|
||||
state = LabelUM(
|
||||
text = TextReference.Str("Warning Label"),
|
||||
style = LabelStyle.WARNING,
|
||||
icon = R.drawable.ic_information_24,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
package com.tangem.core.ui.components.label.entity
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
data class LabelUM(
|
||||
val text: TextReference,
|
||||
val style: LabelStyle,
|
||||
@DrawableRes val icon: Int? = null,
|
||||
val onIconClick: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
enum class LabelStyle {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
|
@ -20,6 +21,7 @@ import com.tangem.core.ui.components.RectangleShimmer
|
|||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.MarketPriceBlockTestTags
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
|
||||
/**
|
||||
|
|
@ -45,7 +47,8 @@ fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier
|
|||
.fillMaxWidth()
|
||||
.heightIn(min = TangemTheme.dimens.size72)
|
||||
.padding(all = TangemTheme.dimens.spacing12)
|
||||
.onSizeChanged { rootWidth = it.width },
|
||||
.onSizeChanged { rootWidth = it.width }
|
||||
.testTag(MarketPriceBlockTestTags.BLOCK),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
|
|
@ -64,6 +67,7 @@ private fun Title(currencyName: String, modifier: Modifier = Modifier) {
|
|||
text = stringResourceSafe(id = R.string.wallet_marketplace_block_title, currencyName),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
modifier = Modifier.testTag(MarketPriceBlockTestTags.TEXT),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
|
|
@ -63,8 +62,8 @@ fun Notification(
|
|||
NotificationConfig.IconTint.Unspecified -> null
|
||||
NotificationConfig.IconTint.Accent -> TangemTheme.colors.icon.accent
|
||||
NotificationConfig.IconTint.Attention -> TangemTheme.colors.icon.attention
|
||||
NotificationConfig.IconTint.Warning -> TangemTheme.colors.icon.warning
|
||||
},
|
||||
iconSize: Dp = 20.dp,
|
||||
isEnabled: Boolean = true,
|
||||
) {
|
||||
NotificationBaseContainer(
|
||||
|
|
@ -78,7 +77,7 @@ fun Notification(
|
|||
MainContent(
|
||||
iconResId = config.iconResId,
|
||||
iconTint = iconTint,
|
||||
iconSize = iconSize,
|
||||
iconSize = config.iconSize,
|
||||
title = config.title,
|
||||
titleColor = titleColor,
|
||||
subtitle = config.subtitle,
|
||||
|
|
@ -117,7 +116,9 @@ internal fun NotificationBaseContainer(
|
|||
) {
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
|
||||
modifier = Modifier
|
||||
.padding(all = TangemTheme.dimens.spacing12)
|
||||
.testTag(NotificationTestTags.CONTAINER),
|
||||
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
content()
|
||||
|
|
@ -152,7 +153,8 @@ private fun MainContent(
|
|||
tint = iconTint,
|
||||
modifier = Modifier
|
||||
.size(size = iconSize)
|
||||
.align(alignment = Alignment.Top),
|
||||
.align(alignment = Alignment.Top)
|
||||
.testTag(NotificationTestTags.ICON),
|
||||
)
|
||||
|
||||
SpacerW(width = TangemTheme.dimens.spacing10)
|
||||
|
|
@ -220,7 +222,7 @@ internal fun TextsBlock(
|
|||
text = subtitleText,
|
||||
color = subtitleColor,
|
||||
style = TangemTheme.typography.caption2,
|
||||
modifier = Modifier.testTag(NotificationTestTags.TEXT),
|
||||
modifier = Modifier.testTag(NotificationTestTags.MESSAGE),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.core.ui.components.notifications
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
|
|
@ -26,6 +28,7 @@ data class NotificationConfig(
|
|||
val onCloseClick: (() -> Unit)? = null,
|
||||
val showArrowIcon: Boolean = onClick != null,
|
||||
val iconTint: IconTint = IconTint.Unspecified,
|
||||
val iconSize: Dp = 20.dp,
|
||||
) {
|
||||
|
||||
sealed class ButtonsState {
|
||||
|
|
@ -62,5 +65,6 @@ data class NotificationConfig(
|
|||
Unspecified,
|
||||
Accent,
|
||||
Attention,
|
||||
Warning,
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.Measurable
|
||||
import androidx.compose.ui.layout.Placeable
|
||||
|
|
@ -309,7 +310,13 @@ private fun CustomContainer(state: TokenItemState, modifier: Modifier = Modifier
|
|||
},
|
||||
)
|
||||
|
||||
fiatAmount?.placeRelative(x = layoutWidth - fiatAmount.width, y = verticalPadding)
|
||||
fiatAmount?.placeRelative(
|
||||
x = layoutWidth - fiatAmount.width,
|
||||
y = when (state.subtitle2State) {
|
||||
null -> (layoutHeight - fiatAmount.height).div(other = 2)
|
||||
else -> verticalPadding
|
||||
},
|
||||
)
|
||||
|
||||
priceChange?.placeRelative(
|
||||
x = icon.width,
|
||||
|
|
@ -462,7 +469,7 @@ private fun calculateLayoutHeight(
|
|||
return max(firstColumnHeight, secondColumnHeight).coerceAtLeast(minLayoutHeight)
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360)
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) {
|
||||
TangemThemePreview(isDark = false) {
|
||||
|
|
@ -589,6 +596,8 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
|
|||
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
|
||||
iconState = customTokenIconState.copy(isGrayscale = true),
|
||||
),
|
||||
AccountItemPreviewData.accountItem,
|
||||
AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon),
|
||||
),
|
||||
) {
|
||||
|
||||
|
|
@ -641,4 +650,38 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object AccountItemPreviewData {
|
||||
val accountResIcon: CurrencyIconState.CryptoPortfolio.Icon
|
||||
get() = CurrencyIconState.CryptoPortfolio.Icon(
|
||||
resId = R.drawable.ic_rounded_star_24,
|
||||
color = Color.Blue,
|
||||
isGrayscale = false,
|
||||
)
|
||||
val accountLetterIcon: CurrencyIconState.CryptoPortfolio.Letter
|
||||
get() = CurrencyIconState.CryptoPortfolio.Letter(
|
||||
char = stringReference("A"),
|
||||
color = Color.Blue,
|
||||
isGrayscale = false,
|
||||
)
|
||||
|
||||
val accountItem: TokenItemState.Content
|
||||
get() = TokenItemState.Content(
|
||||
id = UUID.randomUUID().toString(),
|
||||
iconState = accountResIcon,
|
||||
titleState = TokenItemState.TitleState.Content(
|
||||
text = stringReference(value = "Portfolio"),
|
||||
),
|
||||
fiatAmountState = FiatAmountState.Content(
|
||||
text = "$22,129.65",
|
||||
),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = stringReference("24 tokens"),
|
||||
isAvailable = false,
|
||||
),
|
||||
subtitle2State = null,
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ import java.util.UUID
|
|||
* @param modifier modifier
|
||||
*/
|
||||
@Composable
|
||||
internal fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier: Modifier = Modifier) {
|
||||
fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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
|
||||
import com.tangem.core.ui.test.TransactionHistoryBlockTestTags
|
||||
|
||||
/**
|
||||
* Transactions block title
|
||||
|
|
@ -38,10 +40,13 @@ fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Modifier) {
|
|||
text = stringResourceSafe(id = R.string.common_transactions),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
modifier = Modifier.testTag(TransactionHistoryBlockTestTags.TITLE_TEXT),
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.clickable(onClick = onExploreClick),
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onExploreClick)
|
||||
.testTag(TransactionHistoryBlockTestTags.EXPLORER_ICON),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
|
|
@ -55,6 +60,7 @@ fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Modifier) {
|
|||
text = stringResourceSafe(id = R.string.common_explorer),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
modifier = Modifier.testTag(TransactionHistoryBlockTestTags.EXPLORER_TEXT),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,13 @@ interface ComposableBottomSheetComponent {
|
|||
|
||||
@Composable
|
||||
fun BottomSheet()
|
||||
}
|
||||
|
||||
fun getEmptyComposableBottomSheetComponent() = EmptyComposableBottomSheetComponent
|
||||
|
||||
object EmptyComposableBottomSheetComponent : ComposableBottomSheetComponent {
|
||||
override fun dismiss() {}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.core.ui.decompose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Stable
|
||||
interface ComposableModularContentComponent {
|
||||
|
||||
@Composable
|
||||
fun Title()
|
||||
|
||||
@Composable
|
||||
fun Content(modifier: Modifier)
|
||||
|
||||
@Composable
|
||||
fun Footer()
|
||||
}
|
||||
|
||||
fun getEmptyComposableModularContentComponent() = EmptyComposableModularContentComponent
|
||||
|
||||
object EmptyComposableModularContentComponent : ComposableModularContentComponent {
|
||||
@Composable
|
||||
override fun Title() {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
/* no-op */
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object BaseAmountBlockTestTags {
|
||||
const val PRIMARY_AMOUNT = "STAKING_SEND_DETAILS_SCREEN_PRIMARY_AMOUNT"
|
||||
const val SECONDARY_AMOUNT = "TAKING_SEND_DETAILS_SCREEN_SECONDARY_AMOUNT"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object BaseBottomSheetTestTags {
|
||||
const val ACTION_TITLE = "BASE_BOTTOM_SHEET_ACTION_TITLE"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object BaseDialogTestTags {
|
||||
const val CONTAINER = "BASE_DIALOG_CONTAINER"
|
||||
const val TITLE = "BASE_DIALOG_TITLE"
|
||||
const val TEXT = "BASE_DIALOG_TEXT"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object BaseSearchBarTestTags {
|
||||
const val SEARCH_BAR = "BASE_SEARCH_BAR"
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object DeviceSettingsScreenTestTags {
|
||||
const val LAZY_LIST = "DEVICE_SETTINGS_SCREEN_LAZY_LIST"
|
||||
const val IMAGE_BLOCK = "DEVICE_SETTINGS_SCREEN_IMAGE_BLOCK"
|
||||
const val ITEM_TITLE = "DEVICE_SETTINGS_SCREEN_ITEM_TITLE"
|
||||
const val ITEM_SUBTITLE = "DEVICE_SETTINGS_SCREEN_ITEM_SUBTITLE"
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object DialogTestTags {
|
||||
const val DIALOG_CONTAINER = "DIALOG_CONTAINER"
|
||||
}
|
||||
|
|
@ -5,8 +5,13 @@ object MainScreenTestTags {
|
|||
const val MORE_BUTTON = "MAIN_SCREEN_MORE_BUTTON"
|
||||
const val TOP_BAR = "MAIN_SCREEN_TOP_BAR"
|
||||
const val TOKEN_LIST_ITEM = "MAIN_SCREEN_TOKEN_LIST_ITEM"
|
||||
const val WALLET_BALANCE = "MAIN_SCREEN_WALLET_BALANCE"
|
||||
const val WALLET_LIST_ITEM = "MAIN_SCREEN_WALLET_LIST_ITEM"
|
||||
const val ORGANIZE_TOKENS_BUTTON = "MAIN_SCREEN_ORGANIZE_TOKENS_BUTTON"
|
||||
const val MULTI_CURRENCY_ACTION_BUTTON = "MAIN_SCREEN_MULTI_CURRENCY_ACTION_BUTTON"
|
||||
const val CARD_TITLE = "MAIN_SCREEN_CARD_TITLE"
|
||||
const val CARD_IMAGE = "MAIN_SCREEN_CARD_IMAGE"
|
||||
|
||||
const val WALLET_BALANCE = "MAIN_SCREEN_WALLET_BALANCE"
|
||||
const val TOTAL_BALANCE_MENU_ITEM = "MAIN_SCREEN_TOTAL_BALANCE_MENU_ITEM"
|
||||
const val TOTAL_BALANCE_CONTAINER = "MAIN_SCREEN_TOTAL_BALANCE_CONTAINER"
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object MarketPriceBlockTestTags {
|
||||
const val BLOCK = "MAIN_SCREEN_MARKET_PRICE_BLOCK"
|
||||
const val TEXT = "MAIN_SCREEN_MARKET_PRICE_TEXT"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object MarketsTestTags {
|
||||
const val TOKENS_LIST = "MARKETS_TOKENS_LIST"
|
||||
const val TOKENS_LIST_ITEM = "MARKETS_TOKENS_LIST_ITEM"
|
||||
const val ADD_TO_PORTFOLIO_SWITCH = "MARKETS_ADD_TO_PORTFOLIO_SWITCH"
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object NotificationTestTags {
|
||||
const val CONTAINER = "NOTIFICATION_CONTAINER"
|
||||
const val TITLE = "NOTIFICATION_TITLE"
|
||||
const val TEXT = "NOTIFICATION_TEXT"
|
||||
const val MESSAGE = "NOTIFICATION_MESSAGE"
|
||||
const val ICON = "NOTIFICATION_ICON"
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object ResetCardScreenTestTags {
|
||||
const val TITLE = "RESET_CARD_SCREEN_TITLE"
|
||||
const val ATTENTION_IMAGE = "RESET_CARD_SCREEN_ATTENTION_IMAGE"
|
||||
const val SUBTITLE = "RESET_CARD_SCREEN_SUBTITLE"
|
||||
const val DESCRIPTION = "RESET_CARD_SCREEN_DESCRIPTION"
|
||||
const val CHECKBOX = "RESET_CARD_SCREEN_CHECKBOX"
|
||||
const val CHECKBOX_TEXT = "RESET_CARD_SCREEN_CHECKBOX_TEXT"
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ object SelectCountryBottomSheetTestTags {
|
|||
const val LAZY_LIST = "SELECT_COUNTRY_BOTTOM_SHEET_LAZY_LIST"
|
||||
const val COUNTRY_ITEM = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_ITEM"
|
||||
const val UNAVAILABLE_COUNTRY_ITEM = "SELECT_COUNTRY_BOTTOM_SHEET_UNAVAILABLE_COUNTRY_ITEM"
|
||||
const val SEARCH_BAR = "SELECT_COUNTRY_BOTTOM_SHEET_SEARCH_BAR"
|
||||
const val COUNTRY_ICON = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_ICON"
|
||||
const val UNAVAILABLE_COUNTRY_ICON = "SELECT_COUNTRY_BOTTOM_SHEET_UNAVAILABLE_COUNTRY_ICON"
|
||||
const val COUNTRY_NAME = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_NAME"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object SendAddressScreenTestTags {
|
||||
const val ADDRESS_TEXT_FIELD_TITLE = "SEND_ADDRESS_TEXT_FIELD_TITLE"
|
||||
const val ADDRESS_TEXT_FIELD = "SEND_ADDRESS_TEXT_FIELD"
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object SendScreenTestTags {
|
||||
const val SCREEN_CONTAINER = "SEND_SCREEN_CONTAINER"
|
||||
|
||||
const val AMOUNT_CONTAINER_TITLE = "SEND_SCREEN_AMOUNT_CONTAINER_TITLE"
|
||||
const val AMOUNT_CONTAINER_TEXT = "SEND_SCREEN_AMOUNT_CONTAINER_TEXT"
|
||||
const val INPUT_TEXT_FIELD = "SEND_SCREEN_INPUT_TEXT_FIELD"
|
||||
const val SECONDARY_AMOUNT = "SEND_SCREEN_SECONDARY_AMOUNT"
|
||||
|
||||
const val CURRENCY_BUTTON = "SEND_SCREEN_CURRENCY_BUTTON"
|
||||
const val FIAT_ICON = "SEND_SCREEN_FIAT_ICON"
|
||||
const val CURRENCY_ICON = "SEND_SCREEN_CURRENCY_ICON"
|
||||
const val MAX_BUTTON = "END_SCREEN_MAX_BUTTON"
|
||||
const val PREVIOUS_BUTTON = "SEND_SCREEN_PREVIOUS_BUTTON"
|
||||
}
|
||||
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