Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-06 17:39:50 +03:00
parent 946d60c1c6
commit 11ad540d12
14 changed files with 77 additions and 126 deletions

View file

@ -33,7 +33,6 @@ dependencies {
implementation(deps.timber)
/** Network */
implementation(deps.krateSharedPref)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.okHttp)

View file

@ -1,18 +0,0 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.AppPreferenceStorage
import com.tangem.datasource.local.AppPreferenceStorageImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AppPreferenceStorageModule {
@Binds
@Singleton
fun bindAppPreferenceStorage(appPreferenceStorageImpl: AppPreferenceStorageImpl): AppPreferenceStorage
}

View file

@ -1,13 +0,0 @@
package com.tangem.datasource.local
/**
* Application local storage
*
[REDACTED_AUTHOR]
*/
@Deprecated(message = "Use AppPreferencesStore", level = DeprecationLevel.WARNING)
interface AppPreferenceStorage {
/** Json config with feature toggles 'ToggleName: String - Availability: Boolean' */
var featureToggles: String
}

View file

@ -1,24 +0,0 @@
package com.tangem.datasource.local
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import hu.autsoft.krate.SimpleKrate
import hu.autsoft.krate.default.withDefault
import hu.autsoft.krate.stringPref
import javax.inject.Inject
import javax.inject.Singleton
/**
* Implementation of application local storage
*
* @param context application context
*
[REDACTED_AUTHOR]
*/
@Singleton
internal class AppPreferenceStorageImpl @Inject constructor(
@ApplicationContext context: Context,
) : SimpleKrate(context = context), AppPreferenceStorage {
override var featureToggles: String by stringPref().withDefault("")
}

View file

@ -48,6 +48,8 @@ object PreferencesKeys {
val IS_WALLET_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isWalletSwapPromoShown") }
val IS_TOKEN_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isTokenSwapPromoShown") }
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */

View file

@ -7,14 +7,22 @@ plugins {
}
dependencies {
implementation(project(":core:datasource"))
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Local storages */
implementation(deps.androidx.datastore)
/** Other libraries */
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.timber)
/** Core modules */
implementation(projects.core.datasource)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit)
testImplementation(deps.test.mockk)

View file

@ -11,7 +11,7 @@ import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage
import com.tangem.core.featuretoggle.version.DefaultVersionProvider
import com.tangem.core.featuretoggles.BuildConfig
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.local.AppPreferenceStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -29,7 +29,7 @@ internal object FeatureTogglesManagerModule {
fun provideFeatureTogglesManager(
@ApplicationContext context: Context,
assetReader: AssetReader,
appPreferenceStorage: AppPreferenceStorage,
appPreferencesStore: AppPreferencesStore,
): FeatureTogglesManager {
val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
val localFeatureTogglesStorage = LocalFeatureTogglesStorage(
@ -41,8 +41,7 @@ internal object FeatureTogglesManagerModule {
return if (BuildConfig.TESTER_MENU_ENABLED) {
DevFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
appPreferenceStorage = appPreferenceStorage,
jsonAdapter = moshi.adapter(),
appPreferencesStore = appPreferencesStore,
versionProvider = versionProvider,
)
} else {

View file

@ -1,25 +1,25 @@
package com.tangem.core.featuretoggle.manager
import androidx.annotation.VisibleForTesting
import com.squareup.moshi.JsonAdapter
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.datasource.local.AppPreferenceStorage
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 kotlin.properties.Delegates
/**
* Feature toggles manager implementation in DEV build
*
* @property localFeatureTogglesStorage local feature toggles storage
* @property appPreferenceStorage application local storage
* @property jsonAdapter adapter for parsing json
* @property appPreferencesStore application local store
* @property versionProvider application version provider
*/
internal class DevFeatureTogglesManager(
private val localFeatureTogglesStorage: FeatureTogglesStorage,
private val appPreferenceStorage: AppPreferenceStorage,
private val jsonAdapter: JsonAdapter<Map<String, Boolean>>,
private val appPreferencesStore: AppPreferencesStore,
private val versionProvider: VersionProvider,
) : MutableFeatureTogglesManager {
@ -28,11 +28,9 @@ internal class DevFeatureTogglesManager(
override suspend fun init() {
localFeatureTogglesStorage.init()
val savedFeatureToggles = if (appPreferenceStorage.featureToggles.isNotEmpty()) {
jsonAdapter.fromJson(appPreferenceStorage.featureToggles).orEmpty()
} else {
emptyMap()
}
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
) ?: emptyMap()
featureTogglesMap = localFeatureTogglesStorage.featureToggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())
@ -46,10 +44,10 @@ internal class DevFeatureTogglesManager(
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
override fun changeToggle(name: String, isEnabled: Boolean) {
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
featureTogglesMap[name] ?: return
featureTogglesMap[name] = isEnabled
appPreferenceStorage.featureToggles = jsonAdapter.toJson(featureTogglesMap)
appPreferencesStore.storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, featureTogglesMap)
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)

View file

@ -11,5 +11,5 @@ interface MutableFeatureTogglesManager : FeatureTogglesManager {
fun getFeatureToggles(): Map<String, Boolean>
/** Change availability [isEnabled] of toggle with name [name] */
fun changeToggle(name: String, isEnabled: Boolean)
suspend fun changeToggle(name: String, isEnabled: Boolean)
}

View file

@ -1,5 +1,8 @@
package com.tangem.core.featuretoggle.storage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Data model with information about feature toggle
*
@ -10,4 +13,8 @@ package com.tangem.core.featuretoggle.storage
*
[REDACTED_AUTHOR]
*/
internal data class FeatureToggle(val name: String, val version: String)
@JsonClass(generateAdapter = true)
internal data class FeatureToggle(
@Json(name = "name") val name: String,
@Json(name = "version") val version: String,
)

View file

@ -2,48 +2,42 @@ package com.tangem.core.featuretoggle.manager
import android.annotation.SuppressLint
import com.google.common.truth.Truth
import com.squareup.moshi.JsonAdapter
import com.tangem.core.featuretoggle.storage.FeatureToggle
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
import com.tangem.core.featuretoggle.utils.associateToggles
import com.tangem.core.featuretoggle.version.VersionProvider
import com.tangem.datasource.local.AppPreferenceStorage
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.just
import io.mockk.mockk
import io.mockk.verifyAll
import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi
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 io.mockk.*
import kotlinx.coroutines.test.runTest
import org.junit.Test
import kotlin.collections.set
/**
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalCoroutinesApi::class)
@SuppressLint("CheckResult")
internal class DevFeatureTogglesManagerTest {
private val localFeatureTogglesStorage = mockk<FeatureTogglesStorage>()
private val appPreferenceStorage = mockk<AppPreferenceStorage>(relaxed = true)
private val jsonAdapter = mockk<JsonAdapter<Map<String, Boolean>>>()
private val appPreferenceStore = mockk<AppPreferencesStore>(relaxed = true)
private val versionProvider = mockk<VersionProvider>()
private val manager = DevFeatureTogglesManager(
localFeatureTogglesStorage = localFeatureTogglesStorage,
appPreferenceStorage = appPreferenceStorage,
jsonAdapter = jsonAdapter,
appPreferencesStore = appPreferenceStore,
versionProvider = versionProvider,
)
@Test
fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest {
val currentVersion = "1.0.0"
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles
coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns savedFeatureTogglesMap
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
@ -51,7 +45,6 @@ internal class DevFeatureTogglesManagerTest {
coVerifyOrder {
localFeatureTogglesStorage.init()
jsonAdapter.fromJson(savedFeatureToggles)
versionProvider.get()
}
@ -66,12 +59,12 @@ internal class DevFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if shared prefs kept empty list`() = runTest {
val currentVersion = "1.0.0"
val sharedPrefFeatureToggles = "[]"
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns sharedPrefFeatureToggles
coEvery { jsonAdapter.fromJson(sharedPrefFeatureToggles) } returns savedFeatureTogglesMap
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns emptyMap()
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
@ -79,7 +72,6 @@ internal class DevFeatureTogglesManagerTest {
coVerifyOrder {
localFeatureTogglesStorage.init()
jsonAdapter.fromJson(sharedPrefFeatureToggles)
versionProvider.get()
}
@ -94,10 +86,12 @@ internal class DevFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest {
val currentVersion = "1.0.0"
val currentVersion = "0.1.0"
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns ""
coEvery {
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
} returns null
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns currentVersion
@ -107,7 +101,6 @@ internal class DevFeatureTogglesManagerTest {
localFeatureTogglesStorage.init()
versionProvider.get()
}
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
val expected = localFeatureToggles
.associateToggles(currentVersion)
@ -119,8 +112,7 @@ internal class DevFeatureTogglesManagerTest {
@Test
fun `successfully initialize storage if versionProvider returns null`() = runTest {
coEvery { localFeatureTogglesStorage.init() } just Runs
coEvery { appPreferenceStorage.featureToggles } returns savedFeatureToggles
coEvery { jsonAdapter.fromJson(savedFeatureToggles) } returns savedFeatureTogglesMap
coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles
coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles
coEvery { versionProvider.get() } returns null
@ -128,7 +120,6 @@ internal class DevFeatureTogglesManagerTest {
coVerifyOrder {
localFeatureTogglesStorage.init()
jsonAdapter.fromJson(savedFeatureToggles)
versionProvider.get()
}
@ -180,7 +171,7 @@ internal class DevFeatureTogglesManagerTest {
}
@Test
fun `change toggle that contains in map`() {
fun `change toggle that contains in map`() = runTest {
val changeableToggleName = "INACTIVE_TEST_FEATURE_ENABLED"
val resultMap = mutableMapOf(
changeableToggleName to false,
@ -188,18 +179,16 @@ internal class DevFeatureTogglesManagerTest {
)
manager.setFeatureToggles(resultMap)
coEvery { jsonAdapter.toJson(resultMap) } returns ""
manager.changeToggle(changeableToggleName, true)
resultMap[changeableToggleName] = true
verifyOrder { jsonAdapter.toJson(resultMap) }
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
}
@Test
fun `change toggle that doesn't contains in map`() {
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,
@ -209,8 +198,6 @@ internal class DevFeatureTogglesManagerTest {
manager.changeToggle("FEATURE_TOGGLE", true)
verifyAll(inverse = true) { jsonAdapter.toJson(any()) }
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
}

View file

@ -27,17 +27,18 @@ dependencies {
implementation(projects.domain.appTheme)
implementation(projects.domain.appTheme.models)
/** Core modules */
implementation(project(":core:featuretoggles"))
implementation(project(":core:ui"))
/** Feature Apis */
implementation(project(":features:tester:api"))
/** Other modules */
implementation(project(":libs:crypto"))
/** Other libraries */
implementation(deps.arrow.core)
implementation(deps.timber)
/** Core modules */
implementation(projects.core.featuretoggles)
implementation(projects.core.ui)
implementation(projects.core.utils)
/** Feature Apis */
implementation(projects.features.tester.api)
/** Other modules */
implementation(projects.libs.crypto)
}

View file

@ -4,24 +4,29 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.core.featuretoggle.manager.MutableFeatureTogglesManager
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* ViewModel for screen with list of feature toggles
*
* @property featureTogglesManager manager for getting information about the availability of feature toggles
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
@HiltViewModel
internal class FeatureTogglesViewModel @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
private val dispatchers: CoroutineDispatcherProvider,
) : ViewModel() {
/** Current ui state */
@ -47,9 +52,11 @@ internal class FeatureTogglesViewModel @Inject constructor(
}
private fun onToggleValueChange(name: String, isEnabled: Boolean) {
mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled)
viewModelScope.launch(dispatchers.main) {
mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled)
uiState = uiState.copy(featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles())
uiState = uiState.copy(featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles())
}
}
private fun MutableFeatureTogglesManager.getTesterFeatureToggles(): List<TesterFeatureToggle> {

View file

@ -53,7 +53,6 @@ googlePlayServicesWallet = "19.1.0"
hilt = "2.44"
hilt-navigation = "1.0.0"
jodatime = "2.12.1"
krateSharedPref = "2.0.0"
kotlin-immutable-collections = "0.3.5"
kotsonGsonExt = "2.5.0"
lottie = "3.4.0"
@ -206,7 +205,6 @@ hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" }
hilt-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
hilt-navigation = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
jodatime = { module = "joda-time:joda-time", version.ref = "jodatime" }
krateSharedPref = { module = "hu.autsoft:krate", version.ref = "krateSharedPref" }
kotsonGson = { module = "com.github.salomonbrys.kotson:kotson", version.ref = "kotsonGsonExt" }
lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" }
material = { module = "com.google.android.material:material", version.ref = "googleMaterialComponent" }