diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1fff3a2a88..9de8e3ac3e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -112,6 +112,7 @@ dependencies {
implementation(project(":domain"))
implementation(project(":common"))
implementation(project(":core:analytics"))
+ implementation(project(":core:featuretoggles"))
implementation(project(":core:res"))
implementation(project(":core:ui"))
implementation(project(":core:datasource"))
diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt
index 29e19dad86..69fc9f25f6 100644
--- a/app/src/main/java/com/tangem/tap/TapApplication.kt
+++ b/app/src/main/java/com/tangem/tap/TapApplication.kt
@@ -11,6 +11,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
+import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.AssetReader
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.wallet.BuildConfig
import dagger.hilt.android.HiltAndroidApp
+import kotlinx.coroutines.launch
import okhttp3.logging.HttpLoggingInterceptor
import org.rekotlin.Store
import timber.log.Timber
@@ -119,6 +121,9 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var assetReader: AssetReader
+ @Inject
+ lateinit var featureTogglesManager: FeatureTogglesManager
+
override fun onCreate() {
super.onCreate()
@@ -162,6 +167,10 @@ class TapApplication : Application(), ImageLoaderFactory {
appStateHolder.walletStoresManager = walletStoresManager
store.dispatch(DaggerGraphAction.SetDependencies(assetReader))
+
+ scope.launch {
+ featureTogglesManager.init()
+ }
}
override fun newImageLoader(): ImageLoader {
@@ -211,6 +220,7 @@ class TapApplication : Application(), ImageLoaderFactory {
) {
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply {
appVersion = try {
+ // TODO don't use deprecated method
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
diff --git a/buildSrc/src/main/java/AppConfig.kt b/buildSrc/src/main/java/AppConfig.kt
index 9a7e91768d..5c966326f1 100644
--- a/buildSrc/src/main/java/AppConfig.kt
+++ b/buildSrc/src/main/java/AppConfig.kt
@@ -1,7 +1,7 @@
object AppConfig {
const val packageName = "com.tangem.wallet"
const val versionCode = 1
- const val versionName = "1.0.0.-SNAPSHOT"
+ const val versionName = "1.0.0-SNAPSHOT"
const val minSdkVersion = 21
const val targetSdkVersion = 33
const val compileSdkVersion = 33
diff --git a/buildSrc/src/main/java/ConfigFieldBuilder.kt b/buildSrc/src/main/java/ConfigFieldBuilder.kt
index 42c1f16c56..596ae047d6 100644
--- a/buildSrc/src/main/java/ConfigFieldBuilder.kt
+++ b/buildSrc/src/main/java/ConfigFieldBuilder.kt
@@ -18,9 +18,19 @@ class BuildConfigFieldFactory(
*/
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(),
+ )
}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt
index eaf82306b2..f739b70479 100644
--- a/buildSrc/src/main/java/Dependency.kt
+++ b/buildSrc/src/main/java/Dependency.kt
@@ -47,6 +47,7 @@ object Library {
const val hiltCore = "com.google.dagger:hilt-core:" + Versions.hilt
const val hiltKapt = "com.google.dagger:hilt-compiler:" + Versions.hilt
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 lottie = "com.airbnb.android:lottie:" + Versions.lottie
const val materialComponent = "com.google.android.material:material:" + Versions.googleMaterialComponent
diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt
index 32ba2da6ab..ff548135ef 100644
--- a/buildSrc/src/main/java/Versions.kt
+++ b/buildSrc/src/main/java/Versions.kt
@@ -39,6 +39,7 @@ object Versions {
const val googlePlayServicesWallet = "19.1.0"
const val hilt = "2.44"
const val jodatime = "2.12.1"
+ const val krateSharedPref = "2.0.0"
const val kotsonGsonExt = "2.5.0"
const val lottie = "3.4.0"
const val moshi = "1.12.0"
diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts
index 4cd3a19a15..1199f1f518 100644
--- a/core/datasource/build.gradle.kts
+++ b/core/datasource/build.gradle.kts
@@ -58,12 +58,13 @@ dependencies {
implementation(Library.timber)
/** Network */
- implementation(Library.retrofit)
- implementation(Library.retrofitMoshiConverter)
+ implementation(Library.krateSharedPref)
implementation(Library.moshi)
implementation(Library.moshiKotlin)
implementation(Library.okHttp)
implementation(Library.okHttpLogging)
+ implementation(Library.retrofit)
+ implementation(Library.retrofitMoshiConverter)
/** Time */
implementation(Library.jodatime)
diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt
new file mode 100644
index 0000000000..2eefec13ed
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt
@@ -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
+}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt
new file mode 100644
index 0000000000..3e30c7344b
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt
@@ -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
+}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt
new file mode 100644
index 0000000000..3ce9a8aca9
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt
@@ -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("")
+}
\ No newline at end of file
diff --git a/core/featuretoggles/.gitignore b/core/featuretoggles/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/core/featuretoggles/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/featuretoggles/build.gradle.kts b/core/featuretoggles/build.gradle.kts
new file mode 100644
index 0000000000..84a19a537f
--- /dev/null
+++ b/core/featuretoggles/build.gradle.kts
@@ -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"))
+}
\ No newline at end of file
diff --git a/core/featuretoggles/src/main/AndroidManifest.xml b/core/featuretoggles/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..f96c64f1db
--- /dev/null
+++ b/core/featuretoggles/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json
new file mode 100644
index 0000000000..34473131eb
--- /dev/null
+++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json
@@ -0,0 +1,10 @@
+[
+ {
+ "name": "INACTIVE_TEST_FEATURE_ENABLED",
+ "version": "undefined"
+ },
+ {
+ "name": "ACTIVE2_TEST_FEATURE_ENABLED",
+ "version": "1.0.0"
+ }
+]
\ No newline at end of file
diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt
new file mode 100644
index 0000000000..6b2c82cc51
--- /dev/null
+++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt
@@ -0,0 +1,12 @@
+package com.tangem.core.featuretoggle
+
+/**
+ * Feature toggle
+ *
+[REDACTED_AUTHOR]
+ */
+interface FeatureToggle {
+
+ /** Feature toggle name */
+ val name: String
+}
\ No newline at end of file
diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt
new file mode 100644
index 0000000000..6e5075c705
--- /dev/null
+++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt
@@ -0,0 +1,63 @@
+package com.tangem.core.featuretoggle.contract
+
+import timber.log.Timber
+
+/**
+ * Presentation of application version (..).
+ *
+ * @param value version value as string
+ *
+[REDACTED_AUTHOR]
+ */
+internal class Version private constructor(value: String) : Comparable {
+
+ 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.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
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt
new file mode 100644
index 0000000000..2ee5c72e14
--- /dev/null
+++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt
new file mode 100644
index 0000000000..eadb4bf0fb
--- /dev/null
+++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt
@@ -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,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt
new file mode 100644
index 0000000000..81cf4b9a9d
--- /dev/null
+++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt
@@ -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