Updated on 2026-08-14
This commit is contained in:
commit
89462afb99
75 changed files with 1176 additions and 346 deletions
|
|
@ -47,6 +47,7 @@ android {
|
|||
Field.Environment("prod"),
|
||||
Field.TestActionEnabled(false),
|
||||
Field.LogEnabled(false),
|
||||
Field.TesterMenuAvailability(false),
|
||||
),
|
||||
builder = ::buildConfigField,
|
||||
).create()
|
||||
|
|
@ -67,6 +68,7 @@ android {
|
|||
Field.Environment("dev"),
|
||||
Field.TestActionEnabled(true),
|
||||
Field.LogEnabled(true),
|
||||
Field.TesterMenuAvailability(true),
|
||||
),
|
||||
builder = ::buildConfigField,
|
||||
).create()
|
||||
|
|
@ -77,6 +79,10 @@ android {
|
|||
versionNameSuffix = "-beta"
|
||||
applicationIdSuffix = ".debug"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
BuildConfigFieldFactory(
|
||||
fields = listOf(Field.TesterMenuAvailability(true)),
|
||||
builder = ::buildConfigField,
|
||||
).create()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +118,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"))
|
||||
|
|
@ -126,6 +133,8 @@ dependencies {
|
|||
implementation(project(":features:swap:presentation"))
|
||||
implementation(project(":features:swap:domain"))
|
||||
implementation(project(":features:swap:data"))
|
||||
implementation(project(":features:tester:api"))
|
||||
implementation(project(":features:tester:impl"))
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(AndroidX.coreKtx)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.core.view.WindowInsetsControllerCompat
|
|||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tangem.TangemSdk
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import com.tangem.operations.backup.BackupService
|
||||
import com.tangem.tangem_sdk_new.extensions.init
|
||||
import com.tangem.tangem_sdk_new.extensions.initWithBiometrics
|
||||
|
|
@ -33,6 +34,7 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
|
|||
import com.tangem.tap.features.shop.redux.ShopAction
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeAction
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphAction
|
||||
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.ActivityMainBinding
|
||||
|
|
@ -71,6 +73,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
@Inject
|
||||
lateinit var appStateHolder: AppStateHolder
|
||||
|
||||
/** Router for opening tester menu */
|
||||
@Inject
|
||||
lateinit var testerRouter: TesterRouter
|
||||
|
||||
private var snackbar: Snackbar? = null
|
||||
private val dialogManager = DialogManager()
|
||||
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
|
||||
|
|
@ -97,6 +103,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
GooglePayService(createPaymentsClient(this), this),
|
||||
),
|
||||
)
|
||||
|
||||
store.dispatch(DaggerGraphAction.SetActivityDependencies(testerRouter))
|
||||
}
|
||||
|
||||
private fun initUserWalletsListManager() {
|
||||
|
|
|
|||
|
|
@ -11,12 +11,13 @@ 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.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.DomainLayer
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.tap.common.IntentHandler
|
||||
|
|
@ -49,11 +50,12 @@ 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.network.NetworkConnectivity
|
||||
import com.tangem.tap.persistence.PreferencesStorage
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphAction
|
||||
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,15 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
@Inject
|
||||
lateinit var configManager: ConfigManager
|
||||
|
||||
@Inject
|
||||
lateinit var assetReader: AssetReader
|
||||
|
||||
@Inject
|
||||
lateinit var featureTogglesManager: FeatureTogglesManager
|
||||
|
||||
@Inject
|
||||
lateinit var networkConnectionManager: NetworkConnectionManager
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
|
@ -137,11 +147,9 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
||||
|
||||
DomainLayer.init()
|
||||
NetworkConnectivity.createInstance(store, this)
|
||||
preferencesStorage = PreferencesStorage(this)
|
||||
walletConnectRepository = WalletConnectRepository(this)
|
||||
|
||||
assetReader = AndroidAssetReader(this)
|
||||
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT)
|
||||
initConfigManager(configLoader, ::initWithConfigDependency)
|
||||
initWarningMessagesManager()
|
||||
|
|
@ -155,10 +163,22 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
userTokensRepository = UserTokensRepository.init(
|
||||
context = this,
|
||||
tangemTechService = store.state.domainNetworks.tangemTechService,
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
)
|
||||
appStateHolder.mainStore = store
|
||||
appStateHolder.userTokensRepository = userTokensRepository
|
||||
appStateHolder.walletStoresManager = walletStoresManager
|
||||
|
||||
store.dispatch(
|
||||
action = DaggerGraphAction.SetApplicationDependencies(
|
||||
assetReader = assetReader,
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
),
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
featureTogglesManager.init()
|
||||
}
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
|
|
@ -208,6 +228,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) {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import com.tangem.tap.features.demo.isDemoCard
|
|||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.redux.AddressData
|
||||
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.delay
|
||||
import timber.log.Timber
|
||||
|
|
@ -47,7 +47,8 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
|
|||
} catch (exception: Exception) {
|
||||
Timber.e(exception)
|
||||
|
||||
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
|
||||
val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
|
||||
if (!networkConnectionManager.isOnline) {
|
||||
Result.Failure(TapError.NoInternetConnection)
|
||||
} else {
|
||||
val blockchain = wallet.blockchain
|
||||
|
|
@ -87,8 +88,6 @@ fun WalletManager?.getAddressData(): AddressData? {
|
|||
return if (addressDataList.isEmpty()) null else addressDataList[0]
|
||||
}
|
||||
|
||||
fun WalletManager.getTxHistory(currency: Currency): List<TransactionData> = wallet.getTxHistory(currency)
|
||||
|
||||
fun WalletManager.getBlockchainTxHistory(): List<TransactionData> = wallet.getBlockchainTxHistory()
|
||||
|
||||
fun WalletManager.getTokenTxHistory(token: Token): List<TransactionData> = wallet.getTokenTxHistory(token)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
|
|||
import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeReducer
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphReducer
|
||||
import org.rekotlin.Action
|
||||
|
||||
fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState {
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ import com.tangem.tap.features.walletSelector.redux.WalletSelectorMiddleware
|
|||
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeState
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import org.rekotlin.Middleware
|
||||
import org.rekotlin.StateType
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.common.core.TangemSdkError
|
|||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechService
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.tap.common.AndroidFileReader
|
||||
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
|
||||
|
|
@ -15,7 +16,6 @@ import com.tangem.tap.features.demo.DemoHelper
|
|||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
|
||||
import com.tangem.tap.features.wallet.models.toCurrencies
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -25,6 +25,7 @@ class UserTokensRepository(
|
|||
private val storageService: UserTokensStorageService,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val networkConnectionManager: NetworkConnectionManager,
|
||||
) {
|
||||
|
||||
// TODO("After adding DI") replace with CoroutineDispatcherProvider
|
||||
|
|
@ -34,7 +35,7 @@ class UserTokensRepository(
|
|||
return@withContext loadTokensOffline(card, userId).ifEmpty(::loadDemoCurrencies)
|
||||
}
|
||||
|
||||
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
|
||||
if (!networkConnectionManager.isOnline) {
|
||||
return@withContext loadTokensOffline(card, userId)
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +132,11 @@ class UserTokensRepository(
|
|||
private const val NOT_FOUND_HTTP_CODE = "404"
|
||||
|
||||
// TODO("After adding DI") get dependencies by DI
|
||||
fun init(context: Context, tangemTechService: TangemTechService): UserTokensRepository {
|
||||
fun init(
|
||||
context: Context,
|
||||
tangemTechService: TangemTechService,
|
||||
networkConnectionManager: NetworkConnectionManager,
|
||||
): UserTokensRepository {
|
||||
val fileReader = AndroidFileReader(context)
|
||||
val dispatchers = AppCoroutineDispatcherProvider()
|
||||
|
||||
|
|
@ -149,6 +154,7 @@ class UserTokensRepository(
|
|||
storageService = storageService,
|
||||
tangemTechApi = tangemTechService.api,
|
||||
dispatchers = dispatchers,
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)!!
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ import com.tangem.tap.features.wallet.models.Currency
|
|||
import com.tangem.tap.features.wallet.models.PendingTransactionType
|
||||
import com.tangem.tap.features.wallet.models.filterByCoin
|
||||
import com.tangem.tap.features.wallet.models.getPendingTransactions
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
|
|
@ -62,8 +63,6 @@ internal class DefaultWalletAmountsRepository(
|
|||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : WalletAmountsRepository {
|
||||
private val walletStoresStorage = WalletStoresStorage
|
||||
private val walletManagersStorage = WalletManagerStorage
|
||||
|
||||
override suspend fun updateAmountsForUserWallets(
|
||||
userWallets: List<UserWallet>,
|
||||
|
|
@ -76,8 +75,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
awaitAll(
|
||||
async { fetchAmountsForUserWallets(userWallets) },
|
||||
async { fetchFiatRates(userWallets, walletStores = null, fiatCurrency) },
|
||||
)
|
||||
.fold()
|
||||
).fold()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,8 +102,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
awaitAll(
|
||||
async { fetchAmountForWalletStores(userWalletId, scanResponse, walletStores) },
|
||||
async { fetchFiatRates(listOf(userWallet), walletStores, fiatCurrency) },
|
||||
)
|
||||
.fold()
|
||||
).fold()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -123,41 +120,41 @@ internal class DefaultWalletAmountsRepository(
|
|||
walletStores: List<WalletStoreModel>?,
|
||||
fiatCurrency: FiatCurrency,
|
||||
): CompletionResult<Unit> {
|
||||
// FIXME: Use NetworkConnectionManager when it is added to DI
|
||||
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
|
||||
val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
|
||||
if (!networkConnectionManager.isOnline) {
|
||||
return CompletionResult.Failure(WalletStoresError.NoInternetConnection)
|
||||
}
|
||||
|
||||
val walletStoresInternal = walletStores ?: getWalletStores(userWallets)
|
||||
val currencies = walletStoresInternal
|
||||
.asSequence()
|
||||
.flatMap { it.walletsData }
|
||||
.map { it.currency }
|
||||
val currencies = walletStoresInternal.asSequence().flatMap { it.walletsData }.map { it.currency }
|
||||
|
||||
val coinsIds = currencies.mapNotNull { it.coinId }.distinct().toList()
|
||||
|
||||
return withContext(dispatchers.io) {
|
||||
runCatching { tangemTechApi.getRates(fiatCurrency.code.lowercase(), coinsIds.joinToString(",")) }
|
||||
.onSuccess {
|
||||
updateWalletStoresWithFiatRates(walletStores = walletStoresInternal, fiatRates = it.rates)
|
||||
return@withContext CompletionResult.Success(Unit)
|
||||
}
|
||||
.onFailure {
|
||||
val error = WalletStoresError.FetchFiatRatesError(
|
||||
currencies = currencies.map(Currency::currencySymbol).toList(),
|
||||
cause = it,
|
||||
)
|
||||
runCatching {
|
||||
tangemTechApi.getRates(
|
||||
fiatCurrency.code.lowercase(),
|
||||
coinsIds.joinToString(","),
|
||||
)
|
||||
}.onSuccess {
|
||||
updateWalletStoresWithFiatRates(walletStores = walletStoresInternal, fiatRates = it.rates)
|
||||
return@withContext CompletionResult.Success(Unit)
|
||||
}.onFailure {
|
||||
val error = WalletStoresError.FetchFiatRatesError(
|
||||
currencies = currencies.map(Currency::currencySymbol).toList(),
|
||||
cause = it,
|
||||
)
|
||||
|
||||
Timber.e(
|
||||
error,
|
||||
"""
|
||||
Timber.e(
|
||||
error,
|
||||
"""
|
||||
Unable to fetch fiat rates
|
||||
|- Coins ids: $coinsIds
|
||||
""".trimIndent(),
|
||||
)
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
return@withContext CompletionResult.Failure(error)
|
||||
}
|
||||
return@withContext CompletionResult.Failure(error)
|
||||
}
|
||||
|
||||
error("Unreachable code because runCatching must return result")
|
||||
}
|
||||
|
|
@ -166,9 +163,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
private suspend fun fetchAmountsForUserWallets(
|
||||
userWallets: List<UserWallet>,
|
||||
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
|
||||
userWallets.map { async { fetchAmountsForUserWallet(it) } }
|
||||
.awaitAll()
|
||||
.fold()
|
||||
userWallets.map { async { fetchAmountsForUserWallet(it) } }.awaitAll().fold()
|
||||
}
|
||||
|
||||
private suspend fun fetchAmountsForUserWallet(
|
||||
|
|
@ -186,8 +181,8 @@ internal class DefaultWalletAmountsRepository(
|
|||
scanResponse: ScanResponse,
|
||||
walletStores: List<WalletStoreModel>,
|
||||
): CompletionResult<Unit> = coroutineScope {
|
||||
// FIXME: Use NetworkConnectionManager when it is added to DI
|
||||
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
|
||||
val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
|
||||
if (!networkConnectionManager.isOnline) {
|
||||
walletStores.forEach {
|
||||
updateWalletStoreWithUnreachable(it)
|
||||
}
|
||||
|
|
@ -200,9 +195,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
val walletManager = walletStore.walletManager
|
||||
fetchAmountsForWalletStore(userWalletId, scanResponse, walletStore, walletManager)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
.fold()
|
||||
}.awaitAll().fold()
|
||||
}
|
||||
|
||||
private suspend fun fetchAmountsForWalletStore(
|
||||
|
|
@ -223,24 +216,25 @@ internal class DefaultWalletAmountsRepository(
|
|||
updateWalletStoreWithUnreachable(walletStore)
|
||||
}
|
||||
else -> {
|
||||
updateWalletManager(scanResponse, walletManager)
|
||||
.map { updateWalletManagerInStorage(userWalletId, walletManager) }
|
||||
.flatMap {
|
||||
updateWalletStoreWithAmounts(
|
||||
walletStore = walletStore,
|
||||
updatedWallet = walletManager.wallet,
|
||||
// FIXME: move DemoHelper to Demo core module maybe
|
||||
isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId),
|
||||
)
|
||||
}
|
||||
.flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) }
|
||||
.flatMapOnFailure { error ->
|
||||
updateWalletStoreWithError(
|
||||
walletStore = walletStore,
|
||||
wallet = walletManager.wallet,
|
||||
error = error,
|
||||
)
|
||||
}
|
||||
updateWalletManager(scanResponse, walletManager).map {
|
||||
updateWalletManagerInStorage(
|
||||
userWalletId,
|
||||
walletManager,
|
||||
)
|
||||
}.flatMap {
|
||||
updateWalletStoreWithAmounts(
|
||||
walletStore = walletStore,
|
||||
updatedWallet = walletManager.wallet,
|
||||
// FIXME: move DemoHelper to Demo core module maybe
|
||||
isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId),
|
||||
)
|
||||
}.flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) }.flatMapOnFailure { error ->
|
||||
updateWalletStoreWithError(
|
||||
walletStore = walletStore,
|
||||
wallet = walletManager.wallet,
|
||||
error = error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -267,8 +261,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
walletStore: WalletStoreModel,
|
||||
walletManager: WalletManager,
|
||||
): CompletionResult<Unit> {
|
||||
val rentProvider = walletManager as? RentProvider
|
||||
?: return CompletionResult.Success(Unit)
|
||||
val rentProvider = walletManager as? RentProvider ?: return CompletionResult.Success(Unit)
|
||||
|
||||
when (val result = rentProvider.minimalBalanceForRentExemption()) {
|
||||
is Success -> {
|
||||
|
|
@ -318,7 +311,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
)
|
||||
|
||||
if (error is BlockchainSdkError) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
WalletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletStoreToUpdate = walletStore,
|
||||
update = {
|
||||
|
|
@ -350,7 +343,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
""".trimIndent(),
|
||||
)
|
||||
|
||||
walletStoresStorage.update { prevState ->
|
||||
WalletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletStoreToUpdate = walletStore,
|
||||
update = {
|
||||
|
|
@ -378,7 +371,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
""".trimIndent(),
|
||||
)
|
||||
|
||||
walletStoresStorage.update { prevState ->
|
||||
WalletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletStoreToUpdate = walletStore,
|
||||
update = {
|
||||
|
|
@ -402,7 +395,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
""".trimIndent(),
|
||||
)
|
||||
|
||||
walletStoresStorage.update { prevState ->
|
||||
WalletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletStoreToUpdate = walletStore,
|
||||
update = {
|
||||
|
|
@ -425,7 +418,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
""".trimIndent(),
|
||||
)
|
||||
|
||||
walletStoresStorage.update { prevState ->
|
||||
WalletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStores(
|
||||
walletStoresToUpdate = walletStores,
|
||||
update = {
|
||||
|
|
@ -450,7 +443,7 @@ internal class DefaultWalletAmountsRepository(
|
|||
)
|
||||
|
||||
if (rent != walletStore.walletRent) {
|
||||
walletStoresStorage.update { prevState ->
|
||||
WalletStoresStorage.update { prevState ->
|
||||
prevState.replaceWalletStore(
|
||||
walletStoreToUpdate = walletStore,
|
||||
update = {
|
||||
|
|
@ -465,14 +458,12 @@ internal class DefaultWalletAmountsRepository(
|
|||
userWalletId: UserWalletId,
|
||||
walletManager: WalletManager,
|
||||
) = withContext(Dispatchers.Default) {
|
||||
walletManagersStorage.update { prevManagers ->
|
||||
val newManagersForUserWallet = prevManagers[userWalletId].orEmpty()
|
||||
.toMutableList()
|
||||
.apply {
|
||||
replaceByOrAdd(walletManager) {
|
||||
it.wallet.blockchain == walletManager.wallet.blockchain
|
||||
}
|
||||
WalletManagerStorage.update { prevManagers ->
|
||||
val newManagersForUserWallet = prevManagers[userWalletId].orEmpty().toMutableList().apply {
|
||||
replaceByOrAdd(walletManager) {
|
||||
it.wallet.blockchain == walletManager.wallet.blockchain
|
||||
}
|
||||
}
|
||||
|
||||
prevManagers.apply {
|
||||
set(userWalletId, newManagersForUserWallet)
|
||||
|
|
@ -481,13 +472,8 @@ internal class DefaultWalletAmountsRepository(
|
|||
}
|
||||
|
||||
private suspend fun getWalletStores(userWallets: List<UserWallet>): List<WalletStoreModel> {
|
||||
return userWallets
|
||||
.map { it.walletId }
|
||||
.flatMap { userWalletId ->
|
||||
walletStoresStorage.getAll()
|
||||
.firstOrNull()
|
||||
?.get(userWalletId)
|
||||
.orEmpty()
|
||||
}
|
||||
return userWallets.map { it.walletId }.flatMap { userWalletId ->
|
||||
WalletStoresStorage.getAll().firstOrNull()?.get(userWalletId).orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,8 @@ enum class SettingsElement(
|
|||
AppSettings(R.drawable.ic_settings, R.string.app_settings_title),
|
||||
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
|
||||
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App
|
||||
PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy);
|
||||
PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy),
|
||||
TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
|||
SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null
|
||||
SettingsElement.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != true) it else null
|
||||
SettingsElement.ReferralProgram -> if (cardTypesResolver?.isTangemWallet() == true) it else null
|
||||
SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
|
|
@ -96,6 +97,9 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
|||
SettingsElement.ReferralProgram -> {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram))
|
||||
}
|
||||
SettingsElement.TesterMenu -> {
|
||||
store.state.daggerGraphState.testerRouter?.startTesterScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -32,6 +31,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
|
|||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
|
|
@ -85,7 +85,7 @@ class TokensMiddleware {
|
|||
val loadCoinsService = LoadAvailableCoinsService(
|
||||
tangemTechApi = store.state.domainNetworks.tangemTechService.api,
|
||||
dispatchers = AppCoroutineDispatcherProvider(),
|
||||
assetReader = assetReader,
|
||||
assetReader = store.state.daggerGraphState.get(DaggerGraphState::assetReader),
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
|
|
@ -264,14 +264,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 {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.blockchain.common.AmountType
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.tap.common.analytics.converters.BasicEventsPreChecker
|
||||
import com.tangem.tap.common.analytics.converters.BasicEventsSourceData
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
|
|
@ -37,15 +38,13 @@ import com.tangem.tap.features.wallet.redux.WalletData
|
|||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.redux.WalletStore
|
||||
import com.tangem.tap.features.wallet.redux.reducers.findSelectedCurrency
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.network.NetworkStateChanged
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.totalFiatBalanceCalculator
|
||||
import com.tangem.tap.userWalletsListManager
|
||||
import com.tangem.tap.userWalletsListManagerSafe
|
||||
import com.tangem.tap.walletStoresManager
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -72,6 +71,9 @@ class WalletMiddleware {
|
|||
)
|
||||
}
|
||||
|
||||
private val networkConnectionManager: NetworkConnectionManager
|
||||
get() = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
|
||||
|
||||
val walletMiddleware: Middleware<AppState> = { _, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
|
|
@ -135,16 +137,6 @@ class WalletMiddleware {
|
|||
)
|
||||
}
|
||||
}
|
||||
is NetworkStateChanged -> {
|
||||
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
|
||||
if (!action.isOnline) return
|
||||
|
||||
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync.guard {
|
||||
Timber.e("Unable to proceed with changed network state, no user wallet selected")
|
||||
return
|
||||
}
|
||||
scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet, refresh = true) }
|
||||
}
|
||||
is WalletAction.CopyAddress -> {
|
||||
Analytics.send(Token.Receive.ButtonCopyAddress())
|
||||
action.context.copyToClipboard(action.address)
|
||||
|
|
@ -159,7 +151,7 @@ class WalletMiddleware {
|
|||
store.dispatchOpenUrl(action.exploreUrl)
|
||||
}
|
||||
is WalletAction.Send -> {
|
||||
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
|
||||
if (!networkConnectionManager.isOnline) {
|
||||
store.dispatchErrorNotification(TapError.NoInternetConnection)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,29 +2,26 @@ package com.tangem.tap.features.wallet.redux.middlewares
|
|||
|
||||
import com.tangem.blockchain.common.BlockchainSdkError
|
||||
import com.tangem.blockchain.common.SignatureCountValidator
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.common.CardDTO
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.isGreaterThan
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.domain.extensions.hasSignedHashes
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.network.NetworkConnectivity
|
||||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
||||
class WarningsMiddleware {
|
||||
fun handle(action: WalletAction.Warnings, globalState: GlobalState?) {
|
||||
|
|
@ -75,13 +72,6 @@ class WarningsMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
fun tryToShowAppRatingWarning(wallet: Wallet) {
|
||||
val nonZeroWalletsCount = wallet.amounts.filter {
|
||||
it.value.value?.isGreaterThan(BigDecimal.ZERO) ?: false
|
||||
}.size
|
||||
tryToShowAppRatingWarning(hasNonZeroWallets = nonZeroWalletsCount > 0)
|
||||
}
|
||||
|
||||
private fun showCardWarningsIfNeeded(globalState: GlobalState?) {
|
||||
globalState?.scanResponse?.let { scanResponse ->
|
||||
val card = scanResponse.card
|
||||
|
|
@ -147,7 +137,9 @@ class WarningsMiddleware {
|
|||
|
||||
private fun checkHashesCountOnline() {
|
||||
if (store.state.walletState.hashesCountVerified != false) return
|
||||
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return
|
||||
|
||||
val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
|
||||
if (!networkConnectionManager.isOnline) return
|
||||
|
||||
val scanResponse = store.state.globalState.scanResponse
|
||||
val card = scanResponse?.card
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
package com.tangem.tap.network
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Store
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Deprecated("Use com.tangem.datasource.connection.NetworkConnectionManager")
|
||||
class NetworkConnectivity(
|
||||
private val store: Store<*>,
|
||||
context: Context,
|
||||
) {
|
||||
|
||||
private val wContext: WeakReference<Context> = WeakReference(context)
|
||||
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(c: Context?, intent: Intent?) {
|
||||
store.dispatch(NetworkStateChanged(isOnlineOrConnecting()))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val intentFilter = IntentFilter()
|
||||
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||
wContext.get()?.registerReceiver(receiver, intentFilter)
|
||||
store.dispatch(NetworkStateChanged(isOnlineOrConnecting()))
|
||||
}
|
||||
|
||||
fun isOnlineOrConnecting(): Boolean {
|
||||
val connectivityManager = getConnectivityManager() ?: return false
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
capabilities != null &&
|
||||
(capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET))
|
||||
} else {
|
||||
val networkInfo = connectivityManager.activeNetworkInfo
|
||||
networkInfo != null && networkInfo.isConnectedOrConnecting
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConnectivityManager(): ConnectivityManager? {
|
||||
return wContext.get()?.applicationContext
|
||||
?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var instance: NetworkConnectivity
|
||||
|
||||
fun createInstance(store: Store<*>, context: Context): NetworkConnectivity {
|
||||
instance = NetworkConnectivity(store, context)
|
||||
return instance
|
||||
}
|
||||
|
||||
fun getInstance(): NetworkConnectivity = instance
|
||||
}
|
||||
}
|
||||
|
||||
data class NetworkStateChanged(val isOnline: Boolean) : Action
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.tap.proxy.redux
|
||||
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed interface DaggerGraphAction : Action {
|
||||
|
||||
data class SetApplicationDependencies(
|
||||
val assetReader: AssetReader,
|
||||
val networkConnectionManager: NetworkConnectionManager,
|
||||
) : DaggerGraphAction
|
||||
|
||||
data class SetActivityDependencies(val testerRouter: TesterRouter) : DaggerGraphAction
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.tap.proxy.redux
|
||||
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
object DaggerGraphMiddleware {
|
||||
val daggerGraphMiddleware: Middleware<AppState> = { _, _ ->
|
||||
{ next ->
|
||||
{ action -> next(action) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.tap.proxy.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.SetApplicationDependencies -> state.daggerGraphState.copy(
|
||||
assetReader = action.assetReader,
|
||||
networkConnectionManager = action.networkConnectionManager,
|
||||
)
|
||||
is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy(
|
||||
testerRouter = action.testerRouter,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.tap.proxy.redux
|
||||
|
||||
import com.tangem.datasource.asset.AssetReader
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class DaggerGraphState(
|
||||
val assetReader: AssetReader? = null,
|
||||
val testerRouter: TesterRouter? = null,
|
||||
val networkConnectionManager: NetworkConnectionManager? = null,
|
||||
) : StateType {
|
||||
|
||||
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {
|
||||
return requireNotNull(getDependency()) {
|
||||
"${T::class.simpleName} isn't initialized "
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -15,7 +15,9 @@ object Compose {
|
|||
const val animation = "androidx.compose.animation:animation:" + Versions.compose
|
||||
const val coil = "io.coil-kt:coil-compose:" + Versions.coil
|
||||
const val foundation = "androidx.compose.foundation:foundation:" + Versions.composeFoundation
|
||||
const val hiltNavigation = "androidx.hilt:hilt-navigation-compose:" + Versions.composeHiltNavigation
|
||||
const val material = "androidx.compose.material:material:" + Versions.compose
|
||||
const val navigation = "androidx.navigation:navigation-compose:" + Versions.composeNavigation
|
||||
const val ui = "androidx.compose.ui:ui:" + Versions.compose
|
||||
const val uiTooling = "androidx.compose.ui:ui-tooling:" + Versions.compose
|
||||
const val constraintLayout = "androidx.constraintlayout:constraintlayout-compose:" + Versions.composeConstraint
|
||||
|
|
@ -47,6 +49,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
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ object Versions {
|
|||
// region Compose
|
||||
const val compose = "1.3.1"
|
||||
const val composeFoundation = "1.3.1"
|
||||
const val composeHiltNavigation = "1.0.0"
|
||||
const val composeNavigation = "2.5.1"
|
||||
const val composeConstraint = "1.0.1"
|
||||
// endregion Compose
|
||||
|
||||
|
|
@ -39,6 +41,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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -36,9 +35,9 @@ internal class AndroidNetworkConnectionManager @Inject constructor(
|
|||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : NetworkConnectionManager {
|
||||
|
||||
override val connectionStatus: StateFlow<ConnectionStatus> get() = _connectionStatus
|
||||
override val isOnline: Boolean get() = _isOnline.value
|
||||
|
||||
private val _connectionStatus = MutableStateFlow(ConnectionStatus.OFFLINE)
|
||||
private val _isOnline = MutableStateFlow(value = false)
|
||||
private val callbacks = NetworkConnectionManagerCallbacks()
|
||||
private val receiver = NetworkConnectionBroadcastReceiver()
|
||||
|
||||
|
|
@ -78,28 +77,26 @@ internal class AndroidNetworkConnectionManager @Inject constructor(
|
|||
|
||||
CoroutineScope(SupervisorJob()).launch(dispatchers.io) {
|
||||
try {
|
||||
val status = getCurrentStatus()
|
||||
val status = isConnected()
|
||||
|
||||
_connectionStatus.emit(value = status)
|
||||
Timber.i("Status changed to $status")
|
||||
_isOnline.emit(value = status)
|
||||
Timber.i("Connection status is ${if (status) "ONLINE" else "OFFLINE"}")
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrentStatus(): ConnectionStatus {
|
||||
connectivityManager ?: return ConnectionStatus.OFFLINE
|
||||
private fun isConnected(): Boolean {
|
||||
connectivityManager ?: return false
|
||||
|
||||
val isConnected = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
capabilities != null && capabilities.hasNetworkTransport()
|
||||
} else {
|
||||
val networkInfo = connectivityManager.activeNetworkInfo
|
||||
networkInfo != null && networkInfo.isConnectedOrConnecting
|
||||
}
|
||||
|
||||
return if (isConnected) ConnectionStatus.ONLINE else ConnectionStatus.OFFLINE
|
||||
}
|
||||
|
||||
private fun NetworkCapabilities.hasNetworkTransport(): Boolean {
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.datasource.connection
|
||||
|
||||
/**
|
||||
* Network connection status
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
enum class ConnectionStatus {
|
||||
ONLINE, OFFLINE
|
||||
}
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
package com.tangem.datasource.connection
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** Network connection manager */
|
||||
interface NetworkConnectionManager {
|
||||
|
||||
/** Connection status */
|
||||
val connectionStatus: StateFlow<ConnectionStatus>
|
||||
val isOnline: Boolean
|
||||
}
|
||||
|
|
@ -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,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
|
||||
}
|
||||
|
|
@ -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,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
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,39 @@
|
|||
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? {
|
||||
return runCatching { getVersionName().substringBefore(VERSION_NAME_DELIMITER) }
|
||||
.fold(onSuccess = { it }, onFailure = { null })
|
||||
}
|
||||
|
||||
private fun getVersionName(): String {
|
||||
return 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
|
||||
}
|
||||
}
|
||||
|
||||
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?
|
||||
}
|
||||
1
features/tester/api/.gitignore
vendored
Normal file
1
features/tester/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
36
features/tester/api/build.gradle.kts
Normal file
36
features/tester/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("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 {
|
||||
create("debug_beta") {
|
||||
initWith(getByName("release"))
|
||||
BuildConfigFieldFactory(
|
||||
fields = listOf(
|
||||
Field.Environment("release"),
|
||||
Field.TestActionEnabled(true),
|
||||
Field.LogEnabled(true),
|
||||
),
|
||||
builder = ::buildConfigField,
|
||||
).create()
|
||||
}
|
||||
}
|
||||
}
|
||||
2
features/tester/api/src/main/AndroidManifest.xml
Normal file
2
features/tester/api/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.tangem.features.tester.api" />
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.tester.api
|
||||
|
||||
/**
|
||||
* Outer tester feature router
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface TesterRouter {
|
||||
|
||||
/** Open tester menu */
|
||||
fun startTesterScreen()
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -52,14 +67,22 @@ dependencies {
|
|||
implementation(AndroidX.activityCompose)
|
||||
|
||||
/** Compose */
|
||||
implementation(Compose.foundation)
|
||||
implementation(Compose.hiltNavigation)
|
||||
implementation(Compose.material)
|
||||
implementation(Compose.navigation)
|
||||
implementation(Compose.ui)
|
||||
implementation(Compose.uiTooling)
|
||||
|
||||
/** DI */
|
||||
implementation(Library.accompanistSystemUiController)
|
||||
implementation(Library.hilt)
|
||||
kapt(Library.hiltKapt)
|
||||
|
||||
/** Core modules */
|
||||
implementation(project(":core:featuretoggles"))
|
||||
implementation(project(":core:ui"))
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(project(":features:tester:api"))
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.feature.tester.di
|
||||
|
||||
import com.tangem.feature.tester.presentation.navigation.DefaultTesterRouter
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
internal interface TesterRouterModule {
|
||||
|
||||
@Binds
|
||||
@ActivityScoped
|
||||
fun bindTesterRouter(defaultTesterRouter: DefaultTesterRouter): TesterRouter
|
||||
}
|
||||
|
|
@ -3,15 +3,73 @@ package com.tangem.feature.tester.presentation
|
|||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.tangem.core.ui.components.SystemBarsEffect
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.viewmodels.FeatureTogglesViewModel
|
||||
import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState
|
||||
import com.tangem.feature.tester.presentation.menu.ui.TesterMenuScreen
|
||||
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
|
||||
import com.tangem.feature.tester.presentation.navigation.TesterScreen
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Activity for testers */
|
||||
@AndroidEntryPoint
|
||||
internal class TesterActivity : ComponentActivity() {
|
||||
|
||||
/** Router for inner feature navigation */
|
||||
@Inject
|
||||
lateinit var testerRouter: TesterRouter
|
||||
|
||||
private val innerTesterRouter: InnerTesterRouter
|
||||
get() = requireNotNull(testerRouter as? InnerTesterRouter) {
|
||||
"TesterRouter must be InnerTesterRouter for tester feature"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
// TODO("2949") add logic to screens
|
||||
TangemTheme {
|
||||
val statusBarColor = TangemTheme.colors.background.secondary
|
||||
SystemBarsEffect {
|
||||
setStatusBarColor(color = statusBarColor)
|
||||
}
|
||||
|
||||
TesterNavHost()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TopLevelComposableFunctions")
|
||||
@Composable
|
||||
private fun TesterNavHost() {
|
||||
val navController = rememberNavController().also { innerTesterRouter.setNavController(it) }
|
||||
|
||||
NavHost(navController = navController, startDestination = TesterScreen.MENU.name) {
|
||||
composable(route = TesterScreen.MENU.name) {
|
||||
TesterMenuScreen(
|
||||
state = TesterMenuContentState(
|
||||
onBackClick = innerTesterRouter::back,
|
||||
onFeatureTogglesClick = { innerTesterRouter.open(TesterScreen.FEATURE_TOGGLES) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composable(route = TesterScreen.FEATURE_TOGGLES.name) {
|
||||
val viewModel = hiltViewModel<FeatureTogglesViewModel>().apply {
|
||||
setupNavigation(innerTesterRouter)
|
||||
}
|
||||
|
||||
FeatureTogglesScreen(state = viewModel.uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ package com.tangem.feature.tester.presentation.featuretoggles.models
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class TesterFeatureToggle(
|
||||
internal data class TesterFeatureToggle(
|
||||
val name: String,
|
||||
val isEnabled: Boolean
|
||||
val isEnabled: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.feature.tester.presentation.featuretoggles.state
|
||||
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
|
||||
|
||||
/**
|
||||
* Content state of feature toggles screen
|
||||
*
|
||||
* @property featureToggles feature toggles list
|
||||
* @property onBackClick the lambda to be invoked when back button is pressed
|
||||
* @property onToggleValueChange the lambda to be invoked when switch button is pressed
|
||||
*/
|
||||
internal data class FeatureTogglesContentState(
|
||||
val featureToggles: List<TesterFeatureToggle>,
|
||||
val onBackClick: () -> Unit,
|
||||
val onToggleValueChange: (String, Boolean) -> Unit,
|
||||
)
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.feature.tester.presentation.featuretoggles.state
|
||||
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
|
||||
|
||||
/**
|
||||
* State holder of feature toggles screen
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed interface FeatureTogglesStateHolder {
|
||||
|
||||
/**
|
||||
* Content
|
||||
*
|
||||
* @property featureToggles feature toggles list
|
||||
* @property onToggleValueChange the lambda to be invoked when switch button is pressed
|
||||
* @property onBackClicked the lambda to be invoked when back button is pressed
|
||||
*/
|
||||
data class Content(
|
||||
val onBackClicked: () -> Unit,
|
||||
val featureToggles: List<TesterFeatureToggle>,
|
||||
val onToggleValueChange: (String, Boolean) -> Unit,
|
||||
) : FeatureTogglesStateHolder
|
||||
}
|
||||
|
|
@ -21,23 +21,16 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.tester.impl.R
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesStateHolder
|
||||
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState
|
||||
|
||||
/**
|
||||
* Screen with feature toggles list
|
||||
*
|
||||
* @param stateHolder screen state
|
||||
* @param state screen state
|
||||
*/
|
||||
@Composable
|
||||
fun FeatureTogglesScreen(stateHolder: FeatureTogglesStateHolder) {
|
||||
when (stateHolder) {
|
||||
is FeatureTogglesStateHolder.Content -> FeatureTogglesContent(content = stateHolder)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun FeatureTogglesContent(content: FeatureTogglesStateHolder.Content) {
|
||||
internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -45,14 +38,14 @@ private fun FeatureTogglesContent(content: FeatureTogglesStateHolder.Content) {
|
|||
) {
|
||||
stickyHeader {
|
||||
AppBarWithBackButton(
|
||||
onBackClick = content.onBackClicked,
|
||||
onBackClick = state.onBackClick,
|
||||
text = stringResource(id = R.string.feature_toggles),
|
||||
)
|
||||
}
|
||||
items(content.featureToggles) { featureToggle ->
|
||||
items(state.featureToggles) { featureToggle ->
|
||||
FeatureToggleItem(
|
||||
toggle = featureToggle,
|
||||
onCheckedChange = { isChange -> content.onToggleValueChange(featureToggle.name, isChange) },
|
||||
onCheckedChange = { isChange -> state.onToggleValueChange(featureToggle.name, isChange) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -89,13 +82,13 @@ private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boo
|
|||
private fun PreviewFeatureTogglesScreen_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
FeatureTogglesScreen(
|
||||
stateHolder = FeatureTogglesStateHolder.Content(
|
||||
state = FeatureTogglesContentState(
|
||||
featureToggles = listOf(
|
||||
TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true),
|
||||
TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false),
|
||||
),
|
||||
onToggleValueChange = { _, _ -> },
|
||||
onBackClicked = {},
|
||||
onBackClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -106,13 +99,13 @@ private fun PreviewFeatureTogglesScreen_InLightTheme() {
|
|||
private fun PreviewFeatureTogglesScreen_InDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
FeatureTogglesScreen(
|
||||
stateHolder = FeatureTogglesStateHolder.Content(
|
||||
state = FeatureTogglesContentState(
|
||||
featureToggles = listOf(
|
||||
TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true),
|
||||
TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false),
|
||||
),
|
||||
onToggleValueChange = { _, _ -> },
|
||||
onBackClicked = {},
|
||||
onBackClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.feature.tester.presentation.featuretoggles.viewmodels
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* ViewModel for screen with list of feature toggles
|
||||
*
|
||||
* @property featureTogglesManager manager for getting information about the availability of feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@HiltViewModel
|
||||
internal class FeatureTogglesViewModel @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : ViewModel() {
|
||||
|
||||
/** Current ui state */
|
||||
var uiState: FeatureTogglesContentState by mutableStateOf(initState())
|
||||
private set
|
||||
|
||||
private val mutableFeatureTogglesManager: MutableFeatureTogglesManager
|
||||
get() = requireNotNull(featureTogglesManager as? MutableFeatureTogglesManager) {
|
||||
"Feature toggle manager must be mutable (debug build type)"
|
||||
}
|
||||
|
||||
/** Setup navigation state property by router [router] */
|
||||
fun setupNavigation(router: InnerTesterRouter) {
|
||||
uiState = uiState.copy(onBackClick = router::back)
|
||||
}
|
||||
|
||||
private fun initState(): FeatureTogglesContentState {
|
||||
return FeatureTogglesContentState(
|
||||
featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(),
|
||||
onBackClick = {},
|
||||
onToggleValueChange = ::onToggleValueChange,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onToggleValueChange(name: String, isEnabled: Boolean) {
|
||||
mutableFeatureTogglesManager.changeToggle(name = name, isEnabled = isEnabled)
|
||||
|
||||
uiState = uiState.copy(featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles())
|
||||
}
|
||||
|
||||
private fun MutableFeatureTogglesManager.getTesterFeatureToggles(): List<TesterFeatureToggle> {
|
||||
return this
|
||||
.getFeatureToggles()
|
||||
.map { TesterFeatureToggle(it.key, it.value) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.feature.tester.presentation.menu.state
|
||||
|
||||
/**
|
||||
* Content state of tester menu screen
|
||||
*
|
||||
* @property onBackClick the lambda to be invoked when back button is pressed
|
||||
* @property onFeatureTogglesClick the lambda to be invoked when feature toggles button is pressed
|
||||
*/
|
||||
data class TesterMenuContentState(
|
||||
val onBackClick: () -> Unit,
|
||||
val onFeatureTogglesClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.feature.tester.presentation.menu.state
|
||||
|
||||
/**
|
||||
* State holder of tester menu screen
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed interface TesterMenuStateHolder {
|
||||
|
||||
/**
|
||||
* Content
|
||||
*
|
||||
* @property onBackClicked the lambda to be invoked when back button is pressed
|
||||
* @property onFeatureTogglesClicked the lambda to be invoked when feature toggles button is pressed
|
||||
*/
|
||||
data class Content(
|
||||
val onBackClicked: () -> Unit,
|
||||
val onFeatureTogglesClicked: () -> Unit
|
||||
) : TesterMenuStateHolder
|
||||
}
|
||||
|
|
@ -16,29 +16,22 @@ import com.tangem.core.ui.components.PrimaryButton
|
|||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.tester.impl.R
|
||||
import com.tangem.feature.tester.presentation.menu.state.TesterMenuStateHolder
|
||||
import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState
|
||||
|
||||
/**
|
||||
* Screen with functionality for testers
|
||||
*
|
||||
* @param stateHolder screen state
|
||||
* @param state screen state
|
||||
*/
|
||||
@Composable
|
||||
fun TesterMenuScreen(stateHolder: TesterMenuStateHolder) {
|
||||
when (stateHolder) {
|
||||
is TesterMenuStateHolder.Content -> TesterMenuContent(content = stateHolder)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TesterMenuContent(content: TesterMenuStateHolder.Content) {
|
||||
internal fun TesterMenuScreen(state: TesterMenuContentState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
) {
|
||||
AppBarWithBackButton(
|
||||
onBackClick = content.onBackClicked,
|
||||
onBackClick = state.onBackClick,
|
||||
text = stringResource(id = R.string.tester_menu),
|
||||
)
|
||||
Column(
|
||||
|
|
@ -52,12 +45,12 @@ private fun TesterMenuContent(content: TesterMenuStateHolder.Content) {
|
|||
) {
|
||||
PrimaryButton(
|
||||
text = stringResource(R.string.feature_toggles),
|
||||
onClick = content.onFeatureTogglesClicked,
|
||||
onClick = state.onFeatureTogglesClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
PrimaryButton(
|
||||
text = stringResource(R.string.stand_toggles),
|
||||
onClick = content.onFeatureTogglesClicked,
|
||||
onClick = state.onFeatureTogglesClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = false,
|
||||
)
|
||||
|
|
@ -70,9 +63,9 @@ private fun TesterMenuContent(content: TesterMenuStateHolder.Content) {
|
|||
private fun PreviewTesterMenuScreen_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
TesterMenuScreen(
|
||||
stateHolder = TesterMenuStateHolder.Content(
|
||||
onBackClicked = {},
|
||||
onFeatureTogglesClicked = {},
|
||||
state = TesterMenuContentState(
|
||||
onBackClick = {},
|
||||
onFeatureTogglesClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -83,9 +76,9 @@ private fun PreviewTesterMenuScreen_InLightTheme() {
|
|||
private fun PreviewTesterMenuScreen_InDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
TesterMenuScreen(
|
||||
stateHolder = TesterMenuStateHolder.Content(
|
||||
onBackClicked = {},
|
||||
onFeatureTogglesClicked = {},
|
||||
state = TesterMenuContentState(
|
||||
onBackClick = {},
|
||||
onFeatureTogglesClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.feature.tester.presentation.navigation
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.navigation.NavController
|
||||
import com.tangem.feature.tester.presentation.TesterActivity
|
||||
import dagger.hilt.android.qualifiers.ActivityContext
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Implementation of router for tester feature
|
||||
*
|
||||
* @property context activity context
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@ActivityScoped
|
||||
internal class DefaultTesterRouter @Inject constructor(
|
||||
@ActivityContext private val context: Context,
|
||||
) : InnerTesterRouter {
|
||||
|
||||
private var navController: NavController? = null
|
||||
|
||||
override fun startTesterScreen() {
|
||||
context.startActivity(
|
||||
Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
)
|
||||
}
|
||||
|
||||
override fun setNavController(navController: NavController) {
|
||||
this.navController = navController
|
||||
}
|
||||
|
||||
override fun open(screen: TesterScreen) {
|
||||
navController?.navigate(screen.name)
|
||||
}
|
||||
|
||||
override fun back() {
|
||||
navController?.popBackStack()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.feature.tester.presentation.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import com.tangem.features.tester.api.TesterRouter
|
||||
|
||||
/**
|
||||
* Inner feature router
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal interface InnerTesterRouter : TesterRouter {
|
||||
|
||||
/** Set up a navigation controller that bound to tester navigation graph */
|
||||
fun setNavController(navController: NavController)
|
||||
|
||||
/** Open specified screen [screen] */
|
||||
fun open(screen: TesterScreen)
|
||||
|
||||
/** Open last screen */
|
||||
fun back()
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.feature.tester.presentation.navigation
|
||||
|
||||
/**
|
||||
* Tester feature screens
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal enum class TesterScreen {
|
||||
MENU, FEATURE_TOGGLES
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
@ -23,5 +24,6 @@ include(":features:swap:data")
|
|||
include(":features:swap:domain")
|
||||
include(":features:swap:presentation")
|
||||
|
||||
include(":features:tester:api")
|
||||
include(":features:tester:impl")
|
||||
// endregion Feature modules
|
||||
Loading…
Add table
Add a link
Reference in a new issue