Updated on 2026-08-14
This commit is contained in:
parent
d3f3e86fa3
commit
c118781f5a
30 changed files with 561 additions and 6 deletions
|
|
@ -112,6 +112,7 @@ dependencies {
|
||||||
implementation(project(":domain"))
|
implementation(project(":domain"))
|
||||||
implementation(project(":common"))
|
implementation(project(":common"))
|
||||||
implementation(project(":core:analytics"))
|
implementation(project(":core:analytics"))
|
||||||
|
implementation(project(":core:featuretoggles"))
|
||||||
implementation(project(":core:res"))
|
implementation(project(":core:res"))
|
||||||
implementation(project(":core:ui"))
|
implementation(project(":core:ui"))
|
||||||
implementation(project(":core:datasource"))
|
implementation(project(":core:datasource"))
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||||
import com.tangem.blockchain.common.WalletManagerFactory
|
import com.tangem.blockchain.common.WalletManagerFactory
|
||||||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||||
import com.tangem.core.analytics.Analytics
|
import com.tangem.core.analytics.Analytics
|
||||||
|
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||||
import com.tangem.datasource.api.common.MoshiConverter
|
import com.tangem.datasource.api.common.MoshiConverter
|
||||||
import com.tangem.datasource.asset.AssetReader
|
import com.tangem.datasource.asset.AssetReader
|
||||||
import com.tangem.datasource.config.ConfigManager
|
import com.tangem.datasource.config.ConfigManager
|
||||||
|
|
@ -54,6 +55,7 @@ import com.tangem.tap.persistence.PreferencesStorage
|
||||||
import com.tangem.tap.proxy.AppStateHolder
|
import com.tangem.tap.proxy.AppStateHolder
|
||||||
import com.tangem.wallet.BuildConfig
|
import com.tangem.wallet.BuildConfig
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import org.rekotlin.Store
|
import org.rekotlin.Store
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -119,6 +121,9 @@ class TapApplication : Application(), ImageLoaderFactory {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var assetReader: AssetReader
|
lateinit var assetReader: AssetReader
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var featureTogglesManager: FeatureTogglesManager
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
|
@ -162,6 +167,10 @@ class TapApplication : Application(), ImageLoaderFactory {
|
||||||
appStateHolder.walletStoresManager = walletStoresManager
|
appStateHolder.walletStoresManager = walletStoresManager
|
||||||
|
|
||||||
store.dispatch(DaggerGraphAction.SetDependencies(assetReader))
|
store.dispatch(DaggerGraphAction.SetDependencies(assetReader))
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
featureTogglesManager.init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun newImageLoader(): ImageLoader {
|
||||||
|
|
@ -211,6 +220,7 @@ class TapApplication : Application(), ImageLoaderFactory {
|
||||||
) {
|
) {
|
||||||
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply {
|
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply {
|
||||||
appVersion = try {
|
appVersion = try {
|
||||||
|
// TODO don't use deprecated method
|
||||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||||
pInfo.versionName
|
pInfo.versionName
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
object AppConfig {
|
object AppConfig {
|
||||||
const val packageName = "com.tangem.wallet"
|
const val packageName = "com.tangem.wallet"
|
||||||
const val versionCode = 1
|
const val versionCode = 1
|
||||||
const val versionName = "1.0.0.-SNAPSHOT"
|
const val versionName = "1.0.0-SNAPSHOT"
|
||||||
const val minSdkVersion = 21
|
const val minSdkVersion = 21
|
||||||
const val targetSdkVersion = 33
|
const val targetSdkVersion = 33
|
||||||
const val compileSdkVersion = 33
|
const val compileSdkVersion = 33
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,19 @@ class BuildConfigFieldFactory(
|
||||||
*/
|
*/
|
||||||
sealed class Field(val type: String, val name: String, val value: String) {
|
sealed class Field(val type: String, val name: String, val value: String) {
|
||||||
|
|
||||||
class Environment(value: String) : Field("String", "ENVIRONMENT", "\"$value\"")
|
class Environment(value: String) : Field(type = "String", name = "ENVIRONMENT", value = "\"$value\"")
|
||||||
|
|
||||||
class TestActionEnabled(isEnabled: Boolean) : Field("Boolean","TEST_ACTION_ENABLED", isEnabled.toString())
|
class TestActionEnabled(isEnabled: Boolean) : Field(
|
||||||
|
type = "Boolean",
|
||||||
|
name = "TEST_ACTION_ENABLED",
|
||||||
|
value = isEnabled.toString(),
|
||||||
|
)
|
||||||
|
|
||||||
class LogEnabled(isEnabled: Boolean) : Field("Boolean","LOG_ENABLED", isEnabled.toString())
|
class LogEnabled(isEnabled: Boolean) : Field(type = "Boolean", name = "LOG_ENABLED", value = isEnabled.toString())
|
||||||
|
|
||||||
|
class TesterMenuAvailability(isEnabled: Boolean) : Field(
|
||||||
|
type = "Boolean",
|
||||||
|
name = "TESTER_MENU_ENABLED",
|
||||||
|
value = isEnabled.toString(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +47,7 @@ object Library {
|
||||||
const val hiltCore = "com.google.dagger:hilt-core:" + Versions.hilt
|
const val hiltCore = "com.google.dagger:hilt-core:" + Versions.hilt
|
||||||
const val hiltKapt = "com.google.dagger:hilt-compiler:" + Versions.hilt
|
const val hiltKapt = "com.google.dagger:hilt-compiler:" + Versions.hilt
|
||||||
const val jodatime = "joda-time:joda-time:" + Versions.jodatime
|
const val jodatime = "joda-time:joda-time:" + Versions.jodatime
|
||||||
|
const val krateSharedPref = "hu.autsoft:krate:" + Versions.krateSharedPref
|
||||||
const val kotsonGsonExt = "com.github.salomonbrys.kotson:kotson:" + Versions.kotsonGsonExt
|
const val kotsonGsonExt = "com.github.salomonbrys.kotson:kotson:" + Versions.kotsonGsonExt
|
||||||
const val lottie = "com.airbnb.android:lottie:" + Versions.lottie
|
const val lottie = "com.airbnb.android:lottie:" + Versions.lottie
|
||||||
const val materialComponent = "com.google.android.material:material:" + Versions.googleMaterialComponent
|
const val materialComponent = "com.google.android.material:material:" + Versions.googleMaterialComponent
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ object Versions {
|
||||||
const val googlePlayServicesWallet = "19.1.0"
|
const val googlePlayServicesWallet = "19.1.0"
|
||||||
const val hilt = "2.44"
|
const val hilt = "2.44"
|
||||||
const val jodatime = "2.12.1"
|
const val jodatime = "2.12.1"
|
||||||
|
const val krateSharedPref = "2.0.0"
|
||||||
const val kotsonGsonExt = "2.5.0"
|
const val kotsonGsonExt = "2.5.0"
|
||||||
const val lottie = "3.4.0"
|
const val lottie = "3.4.0"
|
||||||
const val moshi = "1.12.0"
|
const val moshi = "1.12.0"
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,13 @@ dependencies {
|
||||||
implementation(Library.timber)
|
implementation(Library.timber)
|
||||||
|
|
||||||
/** Network */
|
/** Network */
|
||||||
implementation(Library.retrofit)
|
implementation(Library.krateSharedPref)
|
||||||
implementation(Library.retrofitMoshiConverter)
|
|
||||||
implementation(Library.moshi)
|
implementation(Library.moshi)
|
||||||
implementation(Library.moshiKotlin)
|
implementation(Library.moshiKotlin)
|
||||||
implementation(Library.okHttp)
|
implementation(Library.okHttp)
|
||||||
implementation(Library.okHttpLogging)
|
implementation(Library.okHttpLogging)
|
||||||
|
implementation(Library.retrofit)
|
||||||
|
implementation(Library.retrofitMoshiConverter)
|
||||||
|
|
||||||
/** Time */
|
/** Time */
|
||||||
implementation(Library.jodatime)
|
implementation(Library.jodatime)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.tangem.datasource.local
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application local storage
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
interface AppPreferenceStorage {
|
||||||
|
|
||||||
|
/** Json config with feature toggles 'ToggleName: String - Availability: Boolean' */
|
||||||
|
var featureToggles: String
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
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("")
|
||||||
|
}
|
||||||
1
core/featuretoggles/.gitignore
vendored
Normal file
1
core/featuretoggles/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
62
core/featuretoggles/build.gradle.kts
Normal file
62
core/featuretoggles/build.gradle.kts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
kotlin("android")
|
||||||
|
kotlin("kapt")
|
||||||
|
id("com.google.dagger.hilt.android")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
compileSdk = AppConfig.compileSdkVersion
|
||||||
|
minSdk = AppConfig.minSdkVersion
|
||||||
|
targetSdk = AppConfig.targetSdkVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
BuildConfigFieldFactory(
|
||||||
|
fields = listOf(Field.TesterMenuAvailability(false)),
|
||||||
|
builder = ::buildConfigField,
|
||||||
|
).create()
|
||||||
|
}
|
||||||
|
|
||||||
|
debug {
|
||||||
|
BuildConfigFieldFactory(
|
||||||
|
fields = listOf(Field.TesterMenuAvailability(true)),
|
||||||
|
builder = ::buildConfigField,
|
||||||
|
).create()
|
||||||
|
}
|
||||||
|
|
||||||
|
create("debug_beta") {
|
||||||
|
initWith(getByName("release"))
|
||||||
|
BuildConfigFieldFactory(
|
||||||
|
fields = listOf(
|
||||||
|
Field.Environment("release"),
|
||||||
|
Field.TestActionEnabled(true),
|
||||||
|
Field.LogEnabled(true),
|
||||||
|
Field.TesterMenuAvailability(true),
|
||||||
|
),
|
||||||
|
builder = ::buildConfigField,
|
||||||
|
).create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(Library.moshi)
|
||||||
|
implementation(Library.moshiKotlin)
|
||||||
|
implementation(Library.hilt)
|
||||||
|
kapt(Library.hiltKapt)
|
||||||
|
implementation(Library.timber)
|
||||||
|
|
||||||
|
implementation(project(":core:datasource"))
|
||||||
|
}
|
||||||
2
core/featuretoggles/src/main/AndroidManifest.xml
Normal file
2
core/featuretoggles/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.tangem.core.featuretoggles" />
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "INACTIVE_TEST_FEATURE_ENABLED",
|
||||||
|
"version": "undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.tangem.core.featuretoggle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature toggle
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
interface FeatureToggle {
|
||||||
|
|
||||||
|
/** Feature toggle name */
|
||||||
|
val name: String
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.tangem.core.featuretoggle.contract
|
||||||
|
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presentation of application version (<major>.<minor>.<fix?>).
|
||||||
|
*
|
||||||
|
* @param value version value as string
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
internal class Version private constructor(value: String) : Comparable<Version> {
|
||||||
|
|
||||||
|
private val major: Int
|
||||||
|
private val minor: Int
|
||||||
|
private val fix: Int?
|
||||||
|
|
||||||
|
init {
|
||||||
|
val versions = value.split(VERSION_DELIMITER).map(String::toInt)
|
||||||
|
|
||||||
|
major = versions.getVersionValue(index = MAJOR_VERSION_POSITION)
|
||||||
|
minor = versions.getVersionValue(index = MINOR_VERSION_POSITION)
|
||||||
|
fix = versions.getOrNull(index = FIX_VERSION_POSITION)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: Version): Int {
|
||||||
|
var result = major.compareTo(other.major)
|
||||||
|
if (result == 0) result = minor.compareTo(other.minor)
|
||||||
|
if (result == 0) {
|
||||||
|
when {
|
||||||
|
fix == null && other.fix == null -> result = 0
|
||||||
|
fix == null && other.fix != null -> result = -1
|
||||||
|
fix != null && other.fix == null -> result = 1
|
||||||
|
fix != null && other.fix != null -> result = fix.compareTo(other.fix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<Int>.getVersionValue(index: Int): Int {
|
||||||
|
return getOrNull(index) ?: error("Invalid version")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAJOR_VERSION_POSITION = 0
|
||||||
|
private const val MINOR_VERSION_POSITION = 1
|
||||||
|
private const val FIX_VERSION_POSITION = 2
|
||||||
|
private const val VERSION_DELIMITER = "."
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create instance with value [value].
|
||||||
|
* If [value] doesn't meet all requirements, the function returns null.
|
||||||
|
*/
|
||||||
|
fun create(value: String): Version? {
|
||||||
|
return try {
|
||||||
|
Version(value)
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Timber.e(exception, "Version %s is null", value)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.tangem.core.featuretoggle.contract
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version contract to evaluate availability of feature toggle
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
internal object VersionAvailabilityContract {
|
||||||
|
|
||||||
|
private const val DISABLED_FEATURE_TOGGLE_VERSION = "undefined"
|
||||||
|
|
||||||
|
/** Evaluate availability of feature toggles using [currentVersion] and [localVersion] */
|
||||||
|
operator fun invoke(currentVersion: String, localVersion: String): Boolean {
|
||||||
|
if (localVersion == DISABLED_FEATURE_TOGGLE_VERSION) return false
|
||||||
|
val current = Version.create(currentVersion) ?: return false
|
||||||
|
val local = Version.create(localVersion) ?: return false
|
||||||
|
|
||||||
|
return current >= local
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.tangem.core.featuretoggle.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.adapter
|
||||||
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
|
import com.tangem.core.featuretoggle.manager.DevFeatureTogglesManager
|
||||||
|
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||||
|
import com.tangem.core.featuretoggle.manager.ProdFeatureTogglesManager
|
||||||
|
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 dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal object FeatureTogglesManagerModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
fun provideFeatureTogglesManager(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
assetReader: AssetReader,
|
||||||
|
appPreferenceStorage: AppPreferenceStorage,
|
||||||
|
): FeatureTogglesManager {
|
||||||
|
val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
|
||||||
|
val localFeatureTogglesStorage = LocalFeatureTogglesStorage(
|
||||||
|
assetReader = assetReader,
|
||||||
|
jsonAdapter = moshi.adapter(),
|
||||||
|
)
|
||||||
|
val versionProvider = DefaultVersionProvider(context)
|
||||||
|
|
||||||
|
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||||
|
DevFeatureTogglesManager(
|
||||||
|
localFeatureTogglesStorage = localFeatureTogglesStorage,
|
||||||
|
appPreferenceStorage = appPreferenceStorage,
|
||||||
|
jsonAdapter = moshi.adapter(),
|
||||||
|
versionProvider = versionProvider,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ProdFeatureTogglesManager(
|
||||||
|
localFeatureTogglesStorage = localFeatureTogglesStorage,
|
||||||
|
versionProvider = versionProvider,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package com.tangem.core.featuretoggle.manager
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonAdapter
|
||||||
|
import com.tangem.core.featuretoggle.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 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 versionProvider application version provider
|
||||||
|
*/
|
||||||
|
internal class DevFeatureTogglesManager(
|
||||||
|
private val localFeatureTogglesStorage: FeatureTogglesStorage,
|
||||||
|
private val appPreferenceStorage: AppPreferenceStorage,
|
||||||
|
private val jsonAdapter: JsonAdapter<Map<String, Boolean>>,
|
||||||
|
private val versionProvider: VersionProvider,
|
||||||
|
) : MutableFeatureTogglesManager {
|
||||||
|
|
||||||
|
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
||||||
|
|
||||||
|
override suspend fun init() {
|
||||||
|
localFeatureTogglesStorage.init()
|
||||||
|
|
||||||
|
val savedFeatureToggles = if (appPreferenceStorage.featureToggles.isNotEmpty()) {
|
||||||
|
jsonAdapter.fromJson(appPreferenceStorage.featureToggles).orEmpty()
|
||||||
|
} else {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
featureTogglesMap = localFeatureTogglesStorage.featureToggles
|
||||||
|
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
||||||
|
.mapValues { resultToggle ->
|
||||||
|
savedFeatureToggles[resultToggle.key] ?: resultToggle.value
|
||||||
|
}
|
||||||
|
.toMutableMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isFeatureEnabled(toggle: FeatureToggle): Boolean = featureTogglesMap.any { it.key == toggle.name }
|
||||||
|
|
||||||
|
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
|
||||||
|
|
||||||
|
override fun changeToggle(name: String, isEnabled: Boolean) {
|
||||||
|
featureTogglesMap[name] ?: return
|
||||||
|
featureTogglesMap[name] = isEnabled
|
||||||
|
appPreferenceStorage.featureToggles = jsonAdapter.toJson(featureTogglesMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.tangem.core.featuretoggle.manager
|
||||||
|
|
||||||
|
import com.tangem.core.featuretoggle.FeatureToggle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for getting information about the availability of feature toggles
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
interface FeatureTogglesManager {
|
||||||
|
|
||||||
|
/** Initialize manager */
|
||||||
|
suspend fun init()
|
||||||
|
|
||||||
|
/** Check feature toggle [toggle] availability */
|
||||||
|
fun isFeatureEnabled(toggle: FeatureToggle): Boolean
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.tangem.core.featuretoggle.manager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component for change information about the availability of feature toggles
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
interface MutableFeatureTogglesManager : FeatureTogglesManager {
|
||||||
|
|
||||||
|
/** Get feature toggles */
|
||||||
|
fun getFeatureToggles(): Map<String, Boolean>
|
||||||
|
|
||||||
|
/** Change availability [isEnabled] of toggle with name [name] */
|
||||||
|
fun changeToggle(name: String, isEnabled: Boolean)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.tangem.core.featuretoggle.manager
|
||||||
|
|
||||||
|
import com.tangem.core.featuretoggle.FeatureToggle
|
||||||
|
import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage
|
||||||
|
import com.tangem.core.featuretoggle.utils.associateToggles
|
||||||
|
import com.tangem.core.featuretoggle.version.VersionProvider
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feature toggles manager implementation in PROD build
|
||||||
|
*
|
||||||
|
* @property localFeatureTogglesStorage local feature toggles storage
|
||||||
|
* @property versionProvider application version provider
|
||||||
|
*/
|
||||||
|
internal class ProdFeatureTogglesManager(
|
||||||
|
private val localFeatureTogglesStorage: FeatureTogglesStorage,
|
||||||
|
private val versionProvider: VersionProvider,
|
||||||
|
) : FeatureTogglesManager {
|
||||||
|
|
||||||
|
private var featureToggles: Map<String, Boolean> by Delegates.notNull()
|
||||||
|
|
||||||
|
override suspend fun init() {
|
||||||
|
localFeatureTogglesStorage.init()
|
||||||
|
featureToggles = localFeatureTogglesStorage.featureToggles
|
||||||
|
.associateToggles(currentVersion = versionProvider.get() ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isFeatureEnabled(toggle: FeatureToggle): Boolean = featureToggles.any { it.key == toggle.name }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.tangem.core.featuretoggle.storage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data model with information about feature toggle
|
||||||
|
*
|
||||||
|
* @property name feature toggle name
|
||||||
|
* @property version version number in which the feature will be included
|
||||||
|
*
|
||||||
|
* IMPORTANT: if the version is "undefined", it means that feature toggle is disabled!
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
internal data class FeatureToggle(val name: String, val version: String)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.tangem.core.featuretoggle.storage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that initializes and stores a list of feature toggles
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
internal interface FeatureTogglesStorage {
|
||||||
|
|
||||||
|
/** List of feature toggles */
|
||||||
|
val featureToggles: List<FeatureToggle>
|
||||||
|
|
||||||
|
/** Initialize storage */
|
||||||
|
suspend fun init()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.tangem.core.featuretoggle.storage
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonAdapter
|
||||||
|
import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH
|
||||||
|
import com.tangem.datasource.asset.AssetReader
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage implementation for storing local feature toggles.
|
||||||
|
* Feature toggles are declared in file [LOCAL_CONFIG_PATH].
|
||||||
|
*
|
||||||
|
* @property assetReader asset reader
|
||||||
|
* @property jsonAdapter adapter for parsing local json config
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
internal class LocalFeatureTogglesStorage(
|
||||||
|
private val assetReader: AssetReader,
|
||||||
|
private val jsonAdapter: JsonAdapter<List<FeatureToggle>>,
|
||||||
|
) : FeatureTogglesStorage {
|
||||||
|
|
||||||
|
override var featureToggles: List<FeatureToggle> by Delegates.notNull()
|
||||||
|
private set
|
||||||
|
|
||||||
|
override suspend fun init() {
|
||||||
|
runCatching { requireNotNull(jsonAdapter.fromJson(assetReader.readJson(LOCAL_CONFIG_PATH))) }
|
||||||
|
.onSuccess { featureToggles = it }
|
||||||
|
.onFailure { Timber.e(LocalFeatureTogglesStorage::class.java.name, "Failed to parse $LOCAL_CONFIG_PATH") }
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.tangem.core.featuretoggle.utils
|
||||||
|
|
||||||
|
import com.tangem.core.featuretoggle.contract.VersionAvailabilityContract
|
||||||
|
import com.tangem.core.featuretoggle.storage.FeatureToggle
|
||||||
|
|
||||||
|
internal fun List<FeatureToggle>.associateToggles(currentVersion: String): Map<String, Boolean> {
|
||||||
|
return associate { localToggle ->
|
||||||
|
Pair(
|
||||||
|
first = localToggle.name,
|
||||||
|
second = VersionAvailabilityContract(currentVersion, localToggle.version),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.tangem.core.featuretoggle.version
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of application version provider
|
||||||
|
*
|
||||||
|
* @property context application context
|
||||||
|
*/
|
||||||
|
internal class DefaultVersionProvider @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) : VersionProvider {
|
||||||
|
|
||||||
|
override fun get(): String? {
|
||||||
|
val versionName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.packageManager
|
||||||
|
.getPackageInfo(
|
||||||
|
context.packageName,
|
||||||
|
PackageManager.PackageInfoFlags.of(0),
|
||||||
|
)
|
||||||
|
.versionName
|
||||||
|
} else {
|
||||||
|
context.packageManager.getPackageInfo(context.packageName, 0).versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
return runCatching { versionName.substringBefore(VERSION_NAME_DELIMITER) }
|
||||||
|
.fold(onSuccess = { it }, onFailure = { null })
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val VERSION_NAME_DELIMITER = "-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.tangem.core.featuretoggle.version
|
||||||
|
|
||||||
|
/** Application version provider */
|
||||||
|
internal interface VersionProvider {
|
||||||
|
|
||||||
|
/** Get application version */
|
||||||
|
fun get(): String?
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,20 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
release {
|
||||||
|
BuildConfigFieldFactory(
|
||||||
|
fields = listOf(Field.TesterMenuAvailability(false)),
|
||||||
|
builder = ::buildConfigField,
|
||||||
|
).create()
|
||||||
|
}
|
||||||
|
|
||||||
|
debug {
|
||||||
|
BuildConfigFieldFactory(
|
||||||
|
fields = listOf(Field.TesterMenuAvailability(true)),
|
||||||
|
builder = ::buildConfigField,
|
||||||
|
).create()
|
||||||
|
}
|
||||||
|
|
||||||
create("debug_beta") {
|
create("debug_beta") {
|
||||||
initWith(getByName("release"))
|
initWith(getByName("release"))
|
||||||
BuildConfigFieldFactory(
|
BuildConfigFieldFactory(
|
||||||
|
|
@ -32,6 +46,7 @@ android {
|
||||||
Field.Environment("release"),
|
Field.Environment("release"),
|
||||||
Field.TestActionEnabled(true),
|
Field.TestActionEnabled(true),
|
||||||
Field.LogEnabled(true),
|
Field.LogEnabled(true),
|
||||||
|
Field.TesterMenuAvailability(true)
|
||||||
),
|
),
|
||||||
builder = ::buildConfigField,
|
builder = ::buildConfigField,
|
||||||
).create()
|
).create()
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ include(":common")
|
||||||
// region Core modules
|
// region Core modules
|
||||||
include(":core:analytics")
|
include(":core:analytics")
|
||||||
include(":core:datasource")
|
include(":core:datasource")
|
||||||
|
include(":core:featuretoggles")
|
||||||
include(":core:res")
|
include(":core:res")
|
||||||
include(":core:utils")
|
include(":core:utils")
|
||||||
include(":core:ui")
|
include(":core:ui")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue