Updated on 2026-08-14

This commit is contained in:
Tangem 2023-03-06 18:40:52 +03:00
commit 3c1dc271aa
46 changed files with 695 additions and 53 deletions

View file

@ -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"))

View file

@ -11,12 +11,12 @@ 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
import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config
import com.tangem.datasource.utils.AndroidAssetReader
import com.tangem.datasource.utils.AssetReader
import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
import com.tangem.tap.common.IntentHandler
@ -49,11 +49,13 @@ import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.features.di.redux.DaggerGraphAction
import com.tangem.tap.network.NetworkConnectivity
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
@ -66,7 +68,6 @@ lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesStorage
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
lateinit var assetReader: AssetReader
lateinit var userTokensRepository: UserTokensRepository
private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() }
@ -117,6 +118,12 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var configManager: ConfigManager
@Inject
lateinit var assetReader: AssetReader
@Inject
lateinit var featureTogglesManager: FeatureTogglesManager
override fun onCreate() {
super.onCreate()
@ -141,7 +148,6 @@ class TapApplication : Application(), ImageLoaderFactory {
preferencesStorage = PreferencesStorage(this)
walletConnectRepository = WalletConnectRepository(this)
assetReader = AndroidAssetReader(this)
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT)
initConfigManager(configLoader, ::initWithConfigDependency)
initWarningMessagesManager()
@ -159,6 +165,12 @@ class TapApplication : Application(), ImageLoaderFactory {
appStateHolder.mainStore = store
appStateHolder.userTokensRepository = userTokensRepository
appStateHolder.walletStoresManager = walletStoresManager
store.dispatch(DaggerGraphAction.SetDependencies(assetReader))
scope.launch {
featureTogglesManager.init()
}
}
override fun newImageLoader(): ImageLoader {
@ -208,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) {

View file

@ -4,6 +4,7 @@ import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.common.redux.navigation.NavigationReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.di.redux.DaggerGraphReducer
import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer
@ -44,6 +45,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
saveWalletState = SaveWalletReducer.reduce(action, state),
walletSelectorState = WalletSelectorReducer.reduce(action, state),
sprinklrState = SprinklrReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)
}

View file

@ -11,6 +11,8 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.di.redux.DaggerGraphMiddleware
import com.tangem.tap.features.di.redux.DaggerGraphState
import com.tangem.tap.features.disclaimer.redux.DisclaimerMiddleware
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.features.home.redux.HomeMiddleware
@ -64,6 +66,7 @@ data class AppState(
val saveWalletState: SaveWalletState = SaveWalletState(),
val walletSelectorState: WalletSelectorState = WalletSelectorState(),
val sprinklrState: SprinklrState = SprinklrState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
val domainState: DomainState
@ -105,6 +108,7 @@ data class AppState(
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
SprinklrMiddleware().middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,
)
}
}

View file

@ -6,8 +6,8 @@ import com.tangem.common.services.Result
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.datasource.utils.AssetReader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
@ -74,8 +74,8 @@ class LoadAvailableCoinsService(
}
}
fun getTestnetCoins(): List<Currency> {
val json = assetReader.readAssetAsString(FILE_NAME_TESTNET_COINS)
private fun getTestnetCoins(): List<Currency> {
val json = assetReader.readJson(FILE_NAME_TESTNET_COINS)
return currenciesAdapter.fromJson(json)!!.coins
.map { Currency.fromJsonObject(it) }
}

View file

@ -9,10 +9,10 @@ import com.tangem.common.KeyPair
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.datasource.utils.AssetReader
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(
@ -89,7 +89,7 @@ class TwinCardsManager(
}
private fun getIssuers(reader: AssetReader): List<Issuer> {
val file = reader.readAssetAsString("tangem-app-config/issuers")
val file = reader.readJson(fileName = "tangem-app-config/issuers")
return getAdapter().fromJson(file)!!
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.tap.features.di.redux
import com.tangem.datasource.asset.AssetReader
import org.rekotlin.Action
sealed interface DaggerGraphAction : Action {
data class SetDependencies(val assetReader: AssetReader) : DaggerGraphAction
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.di.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Middleware
object DaggerGraphMiddleware {
val daggerGraphMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action -> next(action) }
}
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.features.di.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
object DaggerGraphReducer {
fun reduce(action: Action, state: AppState): DaggerGraphState {
if (action !is DaggerGraphAction) return state.daggerGraphState
return internalReduce(action, state)
}
private fun internalReduce(action: DaggerGraphAction, state: AppState): DaggerGraphState {
return when (action) {
is DaggerGraphAction.SetDependencies -> state.daggerGraphState.copy(
assetReader = action.assetReader,
)
}
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.tap.features.di.redux
import com.tangem.datasource.asset.AssetReader
import org.rekotlin.StateType
data class DaggerGraphState(val assetReader: AssetReader? = null) : StateType

View file

@ -3,7 +3,7 @@ package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.Message
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.datasource.utils.AssetReader
import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager

View file

@ -15,7 +15,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.datasource.utils.AndroidAssetReader
import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
@ -39,10 +39,16 @@ import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@Suppress("LargeClass")
@AndroidEntryPoint
class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
@Inject
lateinit var assetReader: AssetReader
private val mainBinding by lazy { binding.vMain }
private var previousStep: TwinCardsStep = TwinCardsStep.None
@ -253,8 +259,8 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(
TwinCardsAction.Wallet.LaunchFirstStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
AndroidAssetReader(requireContext()),
initialMessage = Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
reader = assetReader,
),
)
}

View file

@ -21,7 +21,6 @@ import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.redux.domainStore
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.assetReader
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
@ -85,7 +84,7 @@ class TokensMiddleware {
val loadCoinsService = LoadAvailableCoinsService(
tangemTechApi = store.state.domainNetworks.tangemTechService.api,
dispatchers = AppCoroutineDispatcherProvider(),
assetReader = assetReader,
assetReader = requireNotNull(store.state.daggerGraphState.assetReader) { "Asset reader is null" },
)
scope.launch {
@ -264,14 +263,10 @@ class TokensMiddleware {
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return DerivationData(
derivations = mapKeyOfWalletPublicKey to toDerive,
)
return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
private class DerivationData(
val derivations: Pair<ByteArrayKey, List<DerivationPath>>,
)
private class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
private fun submitAdd(scanResponse: ScanResponse, currencyList: List<Currency>) {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {

View file

@ -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

View file

@ -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(),
)
}

View file

@ -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

View file

@ -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"

View file

@ -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)

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.asset
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.BufferedReader
import javax.inject.Inject
/**
* Implementation of asset file reader
*
* @property context application context
*/
internal class AndroidAssetReader @Inject constructor(
@ApplicationContext private val context: Context,
) : AssetReader {
override fun readJson(fileName: String): String {
return context.assets
.open("$fileName.json")
.bufferedReader()
.use(BufferedReader::readText)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.asset
/**
* Asset file reader
*
[REDACTED_AUTHOR]
*/
interface AssetReader {
/** Read content of json file [fileName] from asset */
fun readJson(fileName: String): String
}

View file

@ -2,10 +2,10 @@ package com.tangem.datasource.config
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.models.ConfigModel
import com.tangem.datasource.config.models.ConfigValueModel
import com.tangem.datasource.config.models.FeatureModel
import com.tangem.datasource.utils.AssetReader
import timber.log.Timber
/**
@ -25,8 +25,8 @@ class FeaturesLocalLoader(
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
val valuesAdapter: JsonAdapter<ConfigValueModel> = moshi.adapter(ConfigValueModel::class.java)
val jsonFeatures = assetReader.readAssetAsString(featuresName)
val jsonConfigValues = assetReader.readAssetAsString(configValuesName)
val jsonFeatures = assetReader.readJson(featuresName)
val jsonConfigValues = assetReader.readJson(configValuesName)
ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues))
} catch (ex: Exception) {

View file

@ -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
}

View file

@ -0,0 +1,18 @@
package com.tangem.datasource.di
import com.tangem.datasource.asset.AndroidAssetReader
import com.tangem.datasource.asset.AssetReader
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 AssetModule {
@Binds
@Singleton
fun bindAsserReader(androidAssetReader: AndroidAssetReader): AssetReader
}

View file

@ -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
}

View file

@ -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("")
}

View file

@ -1,23 +0,0 @@
package com.tangem.datasource.utils
import android.content.Context
/**
[REDACTED_AUTHOR]
*/
interface AssetReader {
fun readAssetAsString(name: String): String
}
class AndroidAssetReader(
private val context: Context,
) : AssetReader {
override fun readAssetAsString(name: String): String {
return context.readAssetAsString(name)
}
}
fun Context.readAssetAsString(fileName: String): String {
return this.assets.open("$fileName.json").bufferedReader().readText()
}

1
core/featuretoggles/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View 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"))
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.core.featuretoggles" />

View file

@ -0,0 +1,10 @@
[
{
"name": "INACTIVE_TEST_FEATURE_ENABLED",
"version": "undefined"
},
{
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
"version": "1.0.0"
}
]

View file

@ -0,0 +1,12 @@
package com.tangem.core.featuretoggle
/**
* Feature toggle
*
[REDACTED_AUTHOR]
*/
interface FeatureToggle {
/** Feature toggle name */
val name: String
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}

View file

@ -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,
)
}
}
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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 }
}

View file

@ -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)

View file

@ -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()
}

View file

@ -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"
}
}

View file

@ -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),
)
}
}

View file

@ -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 = "-"
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.featuretoggle.version
/** Application version provider */
internal interface VersionProvider {
/** Get application version */
fun get(): String?
}

View file

@ -25,6 +25,20 @@ android {
}
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(
@ -32,6 +46,7 @@ android {
Field.Environment("release"),
Field.TestActionEnabled(true),
Field.LogEnabled(true),
Field.TesterMenuAvailability(true)
),
builder = ::buildConfigField,
).create()

View file

@ -5,6 +5,7 @@ include(":common")
// region Core modules
include(":core:analytics")
include(":core:datasource")
include(":core:featuretoggles")
include(":core:res")
include(":core:utils")
include(":core:ui")