From d3f3e86fa360fb289438edc77a64da17188c13c7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Mar 2023 23:27:34 +0800 Subject: [PATCH 1/4] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 11 +++++---- .../com/tangem/tap/common/redux/AppReducer.kt | 2 ++ .../com/tangem/tap/common/redux/AppState.kt | 4 ++++ .../tokens/LoadAvailableCoinsService.kt | 6 ++--- .../tap/domain/twins/TwinCardsManager.kt | 4 ++-- .../features/di/redux/DaggerGraphAction.kt | 9 ++++++++ .../di/redux/DaggerGraphMiddleware.kt | 12 ++++++++++ .../features/di/redux/DaggerGraphReducer.kt | 20 ++++++++++++++++ .../tap/features/di/redux/DaggerGraphState.kt | 6 +++++ .../products/twins/redux/TwinCardsAction.kt | 2 +- .../products/twins/ui/TwinsCardsFragment.kt | 12 +++++++--- .../features/tokens/redux/TokensMiddleware.kt | 15 +++--------- .../datasource/asset/AndroidAssetReader.kt | 23 +++++++++++++++++++ .../tangem/datasource/asset/AssetReader.kt | 12 ++++++++++ .../datasource/config/FeaturesLocalLoader.kt | 6 ++--- .../com/tangem/datasource/di/AssetModule.kt | 18 +++++++++++++++ .../tangem/datasource/utils/AssetReader.kt | 23 ------------------- 17 files changed, 134 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt create mode 100644 app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphReducer.kt create mode 100644 app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/utils/AssetReader.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 56b14e2732..29e19dad86 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -12,11 +12,10 @@ import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.core.analytics.Analytics import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config -import com.tangem.datasource.utils.AndroidAssetReader -import com.tangem.datasource.utils.AssetReader import com.tangem.domain.DomainLayer import com.tangem.domain.common.LogConfig import com.tangem.tap.common.IntentHandler @@ -49,6 +48,7 @@ import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation import com.tangem.tap.domain.walletconnect.WalletConnectRepository +import com.tangem.tap.features.di.redux.DaggerGraphAction import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.persistence.PreferencesStorage import com.tangem.tap.proxy.AppStateHolder @@ -66,7 +66,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 +116,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var configManager: ConfigManager + @Inject + lateinit var assetReader: AssetReader + override fun onCreate() { super.onCreate() @@ -141,7 +143,6 @@ class TapApplication : Application(), ImageLoaderFactory { preferencesStorage = PreferencesStorage(this) walletConnectRepository = WalletConnectRepository(this) - assetReader = AndroidAssetReader(this) val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) initConfigManager(configLoader, ::initWithConfigDependency) initWarningMessagesManager() @@ -159,6 +160,8 @@ class TapApplication : Application(), ImageLoaderFactory { appStateHolder.mainStore = store appStateHolder.userTokensRepository = userTokensRepository appStateHolder.walletStoresManager = walletStoresManager + + store.dispatch(DaggerGraphAction.SetDependencies(assetReader)) } override fun newImageLoader(): ImageLoader { diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 3bcb47a092..0a85acf922 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -4,6 +4,7 @@ import com.tangem.tap.common.redux.global.globalReducer import com.tangem.tap.common.redux.navigation.NavigationReducer import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer +import com.tangem.tap.features.di.redux.DaggerGraphReducer import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer import com.tangem.tap.features.home.redux.HomeReducer import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer @@ -44,6 +45,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) saveWalletState = SaveWalletReducer.reduce(action, state), walletSelectorState = WalletSelectorReducer.reduce(action, state), sprinklrState = SprinklrReducer.reduce(action, state), + daggerGraphState = DaggerGraphReducer.reduce(action, state), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index e9b253d90d..cd4e19b15f 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -11,6 +11,8 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState +import com.tangem.tap.features.di.redux.DaggerGraphMiddleware +import com.tangem.tap.features.di.redux.DaggerGraphState import com.tangem.tap.features.disclaimer.redux.DisclaimerMiddleware import com.tangem.tap.features.disclaimer.redux.DisclaimerState import com.tangem.tap.features.home.redux.HomeMiddleware @@ -64,6 +66,7 @@ data class AppState( val saveWalletState: SaveWalletState = SaveWalletState(), val walletSelectorState: WalletSelectorState = WalletSelectorState(), val sprinklrState: SprinklrState = SprinklrState(), + val daggerGraphState: DaggerGraphState = DaggerGraphState(), ) : StateType { val domainState: DomainState @@ -105,6 +108,7 @@ data class AppState( LockUserWalletsTimerMiddleware().middleware, AccessCodeRequestPolicyMiddleware().middleware, SprinklrMiddleware().middleware, + DaggerGraphMiddleware.daggerGraphMiddleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt index bc1f1dccc5..e2cf3b1355 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt @@ -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 { - val json = assetReader.readAssetAsString(FILE_NAME_TESTNET_COINS) + private fun getTestnetCoins(): List { + val json = assetReader.readJson(FILE_NAME_TESTNET_COINS) return currenciesAdapter.fromJson(json)!!.coins .map { Currency.fromJsonObject(it) } } diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index 8e2cea35fa..3795a0cf00 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -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 { - val file = reader.readAssetAsString("tangem-app-config/issuers") + val file = reader.readJson(fileName = "tangem-app-config/issuers") return getAdapter().fromJson(file)!! } } diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt new file mode 100644 index 0000000000..6019e1d5b3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.features.di.redux + +import com.tangem.datasource.asset.AssetReader +import org.rekotlin.Action + +sealed interface DaggerGraphAction : Action { + + data class SetDependencies(val assetReader: AssetReader) : DaggerGraphAction +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphMiddleware.kt b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphMiddleware.kt new file mode 100644 index 0000000000..d88f8da24e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphMiddleware.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.features.di.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Middleware + +object DaggerGraphMiddleware { + val daggerGraphMiddleware: Middleware = { _, _ -> + { next -> + { action -> next(action) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphReducer.kt new file mode 100644 index 0000000000..f060e845f9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphReducer.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.features.di.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Action + +object DaggerGraphReducer { + fun reduce(action: Action, state: AppState): DaggerGraphState { + if (action !is DaggerGraphAction) return state.daggerGraphState + + return internalReduce(action, state) + } + + private fun internalReduce(action: DaggerGraphAction, state: AppState): DaggerGraphState { + return when (action) { + is DaggerGraphAction.SetDependencies -> state.daggerGraphState.copy( + assetReader = action.assetReader, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt new file mode 100644 index 0000000000..fe726f8dc0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.features.di.redux + +import com.tangem.datasource.asset.AssetReader +import org.rekotlin.StateType + +data class DaggerGraphState(val assetReader: AssetReader? = null) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt index ac58136a05..c552a354a4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index f471878c85..416dbb70e3 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -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() { + @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() { 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, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 27aa168f02..d072b93e1f 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -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 @@ -87,7 +86,7 @@ class TokensMiddleware { val loadCoinsService = LoadAvailableCoinsService( tangemTechApi = store.state.domainNetworks.tangemTechService.api, dispatchers = AppCoroutineDispatcherProvider(), - assetReader = assetReader, + assetReader = requireNotNull(store.state.daggerGraphState.assetReader) { "Asset reader is null" }, ) scope.launch { @@ -266,18 +265,10 @@ class TokensMiddleware { val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } if (toDerive.isEmpty()) return null - return DerivationData( - derivations = mapKeyOfWalletPublicKey to toDerive, - alreadyDerivedKeys = alreadyDerivedKeys, - mapKeyOfWalletPublicKey = mapKeyOfWalletPublicKey, - ) + return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) } - private class DerivationData( - val derivations: Pair>, - val alreadyDerivedKeys: ExtendedPublicKeysMap, - val mapKeyOfWalletPublicKey: ByteArrayKey, - ) + private class DerivationData(val derivations: Pair>) private fun submitAdd(scanResponse: ScanResponse, currencyList: List) { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt new file mode 100644 index 0000000000..7e33fefae3 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt @@ -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) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt new file mode 100644 index 0000000000..6475ad3909 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt @@ -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 +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt index 8ae508cbd3..829b539552 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/FeaturesLocalLoader.kt @@ -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 = moshi.adapter(FeatureModel::class.java) val valuesAdapter: JsonAdapter = 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) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt new file mode 100644 index 0000000000..7df5502310 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt @@ -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 +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/AssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/AssetReader.kt deleted file mode 100644 index 919d273aea..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/AssetReader.kt +++ /dev/null @@ -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() -} \ No newline at end of file From c118781f5a9d87f83a84825b182116740affe191 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Mar 2023 17:18:36 +0800 Subject: [PATCH 2/4] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../java/com/tangem/tap/TapApplication.kt | 10 +++ buildSrc/src/main/java/AppConfig.kt | 2 +- buildSrc/src/main/java/ConfigFieldBuilder.kt | 16 ++++- buildSrc/src/main/java/Dependency.kt | 1 + buildSrc/src/main/java/Versions.kt | 1 + core/datasource/build.gradle.kts | 5 +- .../di/AppPreferenceStorageModule.kt | 18 ++++++ .../datasource/local/AppPreferenceStorage.kt | 12 ++++ .../local/AppPreferenceStorageImpl.kt | 24 +++++++ core/featuretoggles/.gitignore | 1 + core/featuretoggles/build.gradle.kts | 62 ++++++++++++++++++ .../src/main/AndroidManifest.xml | 2 + .../configs/feature_toggles_config.json | 10 +++ .../core/featuretoggle/FeatureToggle.kt | 12 ++++ .../core/featuretoggle/contract/Version.kt | 63 +++++++++++++++++++ .../contract/VersionAvailabilityContract.kt | 20 ++++++ .../di/FeatureTogglesManagerModule.kt | 55 ++++++++++++++++ .../manager/DevFeatureTogglesManager.kt | 54 ++++++++++++++++ .../manager/FeatureTogglesManager.kt | 17 +++++ .../manager/MutableFeatureTogglesManager.kt | 15 +++++ .../manager/ProdFeatureTogglesManager.kt | 29 +++++++++ .../featuretoggle/storage/FeatureToggle.kt | 13 ++++ .../storage/FeatureTogglesStorage.kt | 15 +++++ .../storage/LocalFeatureTogglesStorage.kt | 35 +++++++++++ .../core/featuretoggle/utils/CollectionExt.kt | 13 ++++ .../version/DefaultVersionProvider.kt | 37 +++++++++++ .../featuretoggle/version/VersionProvider.kt | 8 +++ features/tester/impl/build.gradle.kts | 15 +++++ settings.gradle.kts | 1 + 30 files changed, 561 insertions(+), 6 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt create mode 100644 core/featuretoggles/.gitignore create mode 100644 core/featuretoggles/build.gradle.kts create mode 100644 core/featuretoggles/src/main/AndroidManifest.xml create mode 100644 core/featuretoggles/src/main/assets/configs/feature_toggles_config.json create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureToggle.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/utils/CollectionExt.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt create mode 100644 core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/VersionProvider.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1fff3a2a88..9de8e3ac3e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -112,6 +112,7 @@ dependencies { implementation(project(":domain")) implementation(project(":common")) implementation(project(":core:analytics")) + implementation(project(":core:featuretoggles")) implementation(project(":core:res")) implementation(project(":core:ui")) implementation(project(":core:datasource")) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 29e19dad86..69fc9f25f6 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -11,6 +11,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.core.analytics.Analytics +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.ConfigManager @@ -54,6 +55,7 @@ import com.tangem.tap.persistence.PreferencesStorage import com.tangem.tap.proxy.AppStateHolder import com.tangem.wallet.BuildConfig import dagger.hilt.android.HiltAndroidApp +import kotlinx.coroutines.launch import okhttp3.logging.HttpLoggingInterceptor import org.rekotlin.Store import timber.log.Timber @@ -119,6 +121,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var assetReader: AssetReader + @Inject + lateinit var featureTogglesManager: FeatureTogglesManager + override fun onCreate() { super.onCreate() @@ -162,6 +167,10 @@ class TapApplication : Application(), ImageLoaderFactory { appStateHolder.walletStoresManager = walletStoresManager store.dispatch(DaggerGraphAction.SetDependencies(assetReader)) + + scope.launch { + featureTogglesManager.init() + } } override fun newImageLoader(): ImageLoader { @@ -211,6 +220,7 @@ class TapApplication : Application(), ImageLoaderFactory { ) { fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply { appVersion = try { + // TODO don't use deprecated method val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) pInfo.versionName } catch (e: PackageManager.NameNotFoundException) { diff --git a/buildSrc/src/main/java/AppConfig.kt b/buildSrc/src/main/java/AppConfig.kt index 9a7e91768d..5c966326f1 100644 --- a/buildSrc/src/main/java/AppConfig.kt +++ b/buildSrc/src/main/java/AppConfig.kt @@ -1,7 +1,7 @@ object AppConfig { const val packageName = "com.tangem.wallet" const val versionCode = 1 - const val versionName = "1.0.0.-SNAPSHOT" + const val versionName = "1.0.0-SNAPSHOT" const val minSdkVersion = 21 const val targetSdkVersion = 33 const val compileSdkVersion = 33 diff --git a/buildSrc/src/main/java/ConfigFieldBuilder.kt b/buildSrc/src/main/java/ConfigFieldBuilder.kt index 42c1f16c56..596ae047d6 100644 --- a/buildSrc/src/main/java/ConfigFieldBuilder.kt +++ b/buildSrc/src/main/java/ConfigFieldBuilder.kt @@ -18,9 +18,19 @@ class BuildConfigFieldFactory( */ sealed class Field(val type: String, val name: String, val value: String) { - class Environment(value: String) : Field("String", "ENVIRONMENT", "\"$value\"") + class Environment(value: String) : Field(type = "String", name = "ENVIRONMENT", value = "\"$value\"") - class TestActionEnabled(isEnabled: Boolean) : Field("Boolean","TEST_ACTION_ENABLED", isEnabled.toString()) + class TestActionEnabled(isEnabled: Boolean) : Field( + type = "Boolean", + name = "TEST_ACTION_ENABLED", + value = isEnabled.toString(), + ) - class LogEnabled(isEnabled: Boolean) : Field("Boolean","LOG_ENABLED", isEnabled.toString()) + class LogEnabled(isEnabled: Boolean) : Field(type = "Boolean", name = "LOG_ENABLED", value = isEnabled.toString()) + + class TesterMenuAvailability(isEnabled: Boolean) : Field( + type = "Boolean", + name = "TESTER_MENU_ENABLED", + value = isEnabled.toString(), + ) } \ No newline at end of file diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt index eaf82306b2..f739b70479 100644 --- a/buildSrc/src/main/java/Dependency.kt +++ b/buildSrc/src/main/java/Dependency.kt @@ -47,6 +47,7 @@ object Library { const val hiltCore = "com.google.dagger:hilt-core:" + Versions.hilt const val hiltKapt = "com.google.dagger:hilt-compiler:" + Versions.hilt const val jodatime = "joda-time:joda-time:" + Versions.jodatime + const val krateSharedPref = "hu.autsoft:krate:" + Versions.krateSharedPref const val kotsonGsonExt = "com.github.salomonbrys.kotson:kotson:" + Versions.kotsonGsonExt const val lottie = "com.airbnb.android:lottie:" + Versions.lottie const val materialComponent = "com.google.android.material:material:" + Versions.googleMaterialComponent diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index 32ba2da6ab..ff548135ef 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -39,6 +39,7 @@ object Versions { const val googlePlayServicesWallet = "19.1.0" const val hilt = "2.44" const val jodatime = "2.12.1" + const val krateSharedPref = "2.0.0" const val kotsonGsonExt = "2.5.0" const val lottie = "3.4.0" const val moshi = "1.12.0" diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 4cd3a19a15..1199f1f518 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -58,12 +58,13 @@ dependencies { implementation(Library.timber) /** Network */ - implementation(Library.retrofit) - implementation(Library.retrofitMoshiConverter) + implementation(Library.krateSharedPref) implementation(Library.moshi) implementation(Library.moshiKotlin) implementation(Library.okHttp) implementation(Library.okHttpLogging) + implementation(Library.retrofit) + implementation(Library.retrofitMoshiConverter) /** Time */ implementation(Library.jodatime) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt new file mode 100644 index 0000000000..2eefec13ed --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppPreferenceStorageModule.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.AppPreferenceStorage +import com.tangem.datasource.local.AppPreferenceStorageImpl +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface AppPreferenceStorageModule { + + @Binds + @Singleton + fun bindAppPreferenceStorage(appPreferenceStorageImpl: AppPreferenceStorageImpl): AppPreferenceStorage +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt new file mode 100644 index 0000000000..3e30c7344b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorage.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.local + +/** + * Application local storage + * +[REDACTED_AUTHOR] + */ +interface AppPreferenceStorage { + + /** Json config with feature toggles 'ToggleName: String - Availability: Boolean' */ + var featureToggles: String +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt new file mode 100644 index 0000000000..3ce9a8aca9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/AppPreferenceStorageImpl.kt @@ -0,0 +1,24 @@ +package com.tangem.datasource.local + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import hu.autsoft.krate.SimpleKrate +import hu.autsoft.krate.default.withDefault +import hu.autsoft.krate.stringPref +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Implementation of application local storage + * + * @param context application context + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class AppPreferenceStorageImpl @Inject constructor( + @ApplicationContext context: Context, +) : SimpleKrate(context = context), AppPreferenceStorage { + + override var featureToggles: String by stringPref().withDefault("") +} \ No newline at end of file diff --git a/core/featuretoggles/.gitignore b/core/featuretoggles/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/featuretoggles/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/featuretoggles/build.gradle.kts b/core/featuretoggles/build.gradle.kts new file mode 100644 index 0000000000..84a19a537f --- /dev/null +++ b/core/featuretoggles/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") + id("com.google.dagger.hilt.android") +} + +android { + defaultConfig { + compileSdk = AppConfig.compileSdkVersion + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + buildTypes { + release { + BuildConfigFieldFactory( + fields = listOf(Field.TesterMenuAvailability(false)), + builder = ::buildConfigField, + ).create() + } + + debug { + BuildConfigFieldFactory( + fields = listOf(Field.TesterMenuAvailability(true)), + builder = ::buildConfigField, + ).create() + } + + create("debug_beta") { + initWith(getByName("release")) + BuildConfigFieldFactory( + fields = listOf( + Field.Environment("release"), + Field.TestActionEnabled(true), + Field.LogEnabled(true), + Field.TesterMenuAvailability(true), + ), + builder = ::buildConfigField, + ).create() + } + } +} + +dependencies { + implementation(Library.moshi) + implementation(Library.moshiKotlin) + implementation(Library.hilt) + kapt(Library.hiltKapt) + implementation(Library.timber) + + implementation(project(":core:datasource")) +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/AndroidManifest.xml b/core/featuretoggles/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..f96c64f1db --- /dev/null +++ b/core/featuretoggles/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json new file mode 100644 index 0000000000..34473131eb --- /dev/null +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -0,0 +1,10 @@ +[ + { + "name": "INACTIVE_TEST_FEATURE_ENABLED", + "version": "undefined" + }, + { + "name": "ACTIVE2_TEST_FEATURE_ENABLED", + "version": "1.0.0" + } +] \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt new file mode 100644 index 0000000000..6b2c82cc51 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/FeatureToggle.kt @@ -0,0 +1,12 @@ +package com.tangem.core.featuretoggle + +/** + * Feature toggle + * +[REDACTED_AUTHOR] + */ +interface FeatureToggle { + + /** Feature toggle name */ + val name: String +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt new file mode 100644 index 0000000000..6e5075c705 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/Version.kt @@ -0,0 +1,63 @@ +package com.tangem.core.featuretoggle.contract + +import timber.log.Timber + +/** + * Presentation of application version (..). + * + * @param value version value as string + * +[REDACTED_AUTHOR] + */ +internal class Version private constructor(value: String) : Comparable { + + private val major: Int + private val minor: Int + private val fix: Int? + + init { + val versions = value.split(VERSION_DELIMITER).map(String::toInt) + + major = versions.getVersionValue(index = MAJOR_VERSION_POSITION) + minor = versions.getVersionValue(index = MINOR_VERSION_POSITION) + fix = versions.getOrNull(index = FIX_VERSION_POSITION) + } + + override fun compareTo(other: Version): Int { + var result = major.compareTo(other.major) + if (result == 0) result = minor.compareTo(other.minor) + if (result == 0) { + when { + fix == null && other.fix == null -> result = 0 + fix == null && other.fix != null -> result = -1 + fix != null && other.fix == null -> result = 1 + fix != null && other.fix != null -> result = fix.compareTo(other.fix) + } + } + return result + } + + private fun List.getVersionValue(index: Int): Int { + return getOrNull(index) ?: error("Invalid version") + } + + companion object { + private const val MAJOR_VERSION_POSITION = 0 + private const val MINOR_VERSION_POSITION = 1 + private const val FIX_VERSION_POSITION = 2 + private const val VERSION_DELIMITER = "." + + /** + * Create instance with value [value]. + * If [value] doesn't meet all requirements, the function returns null. + */ + fun create(value: String): Version? { + return try { + Version(value) + } catch (exception: Exception) { + Timber.e(exception, "Version %s is null", value) + return null + } + } + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt new file mode 100644 index 0000000000..2ee5c72e14 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt @@ -0,0 +1,20 @@ +package com.tangem.core.featuretoggle.contract + +/** + * Version contract to evaluate availability of feature toggle + * +[REDACTED_AUTHOR] + */ +internal object VersionAvailabilityContract { + + private const val DISABLED_FEATURE_TOGGLE_VERSION = "undefined" + + /** Evaluate availability of feature toggles using [currentVersion] and [localVersion] */ + operator fun invoke(currentVersion: String, localVersion: String): Boolean { + if (localVersion == DISABLED_FEATURE_TOGGLE_VERSION) return false + val current = Version.create(currentVersion) ?: return false + val local = Version.create(localVersion) ?: return false + + return current >= local + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt new file mode 100644 index 0000000000..eadb4bf0fb --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt @@ -0,0 +1,55 @@ +package com.tangem.core.featuretoggle.di + +import android.content.Context +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.core.featuretoggle.manager.DevFeatureTogglesManager +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.featuretoggle.manager.ProdFeatureTogglesManager +import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage +import com.tangem.core.featuretoggle.version.DefaultVersionProvider +import com.tangem.core.featuretoggles.BuildConfig +import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.local.AppPreferenceStorage +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object FeatureTogglesManagerModule { + + @Provides + @Singleton + @OptIn(ExperimentalStdlibApi::class) + fun provideFeatureTogglesManager( + @ApplicationContext context: Context, + assetReader: AssetReader, + appPreferenceStorage: AppPreferenceStorage, + ): FeatureTogglesManager { + val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() + val localFeatureTogglesStorage = LocalFeatureTogglesStorage( + assetReader = assetReader, + jsonAdapter = moshi.adapter(), + ) + val versionProvider = DefaultVersionProvider(context) + + return if (BuildConfig.TESTER_MENU_ENABLED) { + DevFeatureTogglesManager( + localFeatureTogglesStorage = localFeatureTogglesStorage, + appPreferenceStorage = appPreferenceStorage, + jsonAdapter = moshi.adapter(), + versionProvider = versionProvider, + ) + } else { + ProdFeatureTogglesManager( + localFeatureTogglesStorage = localFeatureTogglesStorage, + versionProvider = versionProvider, + ) + } + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt new file mode 100644 index 0000000000..81cf4b9a9d --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt @@ -0,0 +1,54 @@ +package com.tangem.core.featuretoggle.manager + +import com.squareup.moshi.JsonAdapter +import com.tangem.core.featuretoggle.FeatureToggle +import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage +import com.tangem.core.featuretoggle.utils.associateToggles +import com.tangem.core.featuretoggle.version.VersionProvider +import com.tangem.datasource.local.AppPreferenceStorage +import kotlin.properties.Delegates + +/** + * Feature toggles manager implementation in DEV build + * + * @property localFeatureTogglesStorage local feature toggles storage + * @property appPreferenceStorage application local storage + * @property jsonAdapter adapter for parsing json + * @property versionProvider application version provider + */ +internal class DevFeatureTogglesManager( + private val localFeatureTogglesStorage: FeatureTogglesStorage, + private val appPreferenceStorage: AppPreferenceStorage, + private val jsonAdapter: JsonAdapter>, + private val versionProvider: VersionProvider, +) : MutableFeatureTogglesManager { + + private var featureTogglesMap: MutableMap 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 = featureTogglesMap + + override fun changeToggle(name: String, isEnabled: Boolean) { + featureTogglesMap[name] ?: return + featureTogglesMap[name] = isEnabled + appPreferenceStorage.featureToggles = jsonAdapter.toJson(featureTogglesMap) + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt new file mode 100644 index 0000000000..37cb34b343 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt @@ -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 +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt new file mode 100644 index 0000000000..abf6ff5b72 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt @@ -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 + + /** Change availability [isEnabled] of toggle with name [name] */ + fun changeToggle(name: String, isEnabled: Boolean) +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt new file mode 100644 index 0000000000..bbfe8641d2 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt @@ -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 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 } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureToggle.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureToggle.kt new file mode 100644 index 0000000000..69de7b76e7 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureToggle.kt @@ -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) \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt new file mode 100644 index 0000000000..dd788efacb --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt @@ -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 + + /** Initialize storage */ + suspend fun init() +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt new file mode 100644 index 0000000000..8cd19f39d4 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt @@ -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>, +) : FeatureTogglesStorage { + + override var featureToggles: List 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" + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/utils/CollectionExt.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/utils/CollectionExt.kt new file mode 100644 index 0000000000..990f95cf18 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/utils/CollectionExt.kt @@ -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.associateToggles(currentVersion: String): Map { + return associate { localToggle -> + Pair( + first = localToggle.name, + second = VersionAvailabilityContract(currentVersion, localToggle.version), + ) + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt new file mode 100644 index 0000000000..ab449b92ec --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt @@ -0,0 +1,37 @@ +package com.tangem.core.featuretoggle.version + +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +/** + * Implementation of application version provider + * + * @property context application context + */ +internal class DefaultVersionProvider @Inject constructor( + @ApplicationContext private val context: Context, +) : VersionProvider { + + override fun get(): String? { + val versionName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager + .getPackageInfo( + context.packageName, + PackageManager.PackageInfoFlags.of(0), + ) + .versionName + } else { + context.packageManager.getPackageInfo(context.packageName, 0).versionName + } + + return runCatching { versionName.substringBefore(VERSION_NAME_DELIMITER) } + .fold(onSuccess = { it }, onFailure = { null }) + } + + private companion object { + const val VERSION_NAME_DELIMITER = "-" + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/VersionProvider.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/VersionProvider.kt new file mode 100644 index 0000000000..585caf85b8 --- /dev/null +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/VersionProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.core.featuretoggle.version + +/** Application version provider */ +internal interface VersionProvider { + + /** Get application version */ + fun get(): String? +} \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 8466265b89..d32128a56b 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -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() diff --git a/settings.gradle.kts b/settings.gradle.kts index 75add91dd0..da8fd1aacc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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") From 0d2f6e24108f784069d506b88e5f0f9a1ac9f5ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Mar 2023 16:09:51 +0800 Subject: [PATCH 3/4] Updated on 2026-08-14 --- app/build.gradle.kts | 8 +++ .../main/java/com/tangem/tap/MainActivity.kt | 8 +++ .../java/com/tangem/tap/TapApplication.kt | 4 +- .../com/tangem/tap/common/redux/AppReducer.kt | 2 +- .../com/tangem/tap/common/redux/AppState.kt | 4 +- .../details/ui/details/DetailsScreenState.kt | 3 +- .../details/ui/details/DetailsViewModel.kt | 4 ++ .../features/di/redux/DaggerGraphAction.kt | 9 --- .../tap/features/di/redux/DaggerGraphState.kt | 6 -- .../tap/proxy/redux/DaggerGraphAction.kt | 12 ++++ .../redux/DaggerGraphMiddleware.kt | 2 +- .../di => proxy}/redux/DaggerGraphReducer.kt | 9 ++- .../tap/proxy/redux/DaggerGraphState.kt | 10 ++++ buildSrc/src/main/java/Dependency.kt | 2 + buildSrc/src/main/java/Versions.kt | 2 + .../version/DefaultVersionProvider.kt | 10 ++-- features/tester/api/.gitignore | 1 + features/tester/api/build.gradle.kts | 36 +++++++++++ .../tester/api/src/main/AndroidManifest.xml | 2 + .../features/tester/api/TesterRouter.kt | 12 ++++ features/tester/impl/build.gradle.kts | 10 +++- .../feature/tester/di/TesterRouterModule.kt | 18 ++++++ .../tester/presentation/TesterActivity.kt | 60 ++++++++++++++++++- .../models/TesterFeatureToggle.kt | 4 +- .../state/FeatureTogglesContentState.kt | 16 +++++ .../state/FeatureTogglesStateHolder.kt | 24 -------- .../featuretoggles/ui/FeatureTogglesScreen.kt | 27 ++++----- .../viewmodels/FeatureTogglesViewModel.kt | 60 +++++++++++++++++++ .../menu/state/TesterMenuContentState.kt | 12 ++++ .../menu/state/TesterMenuStateHolder.kt | 20 ------- .../presentation/menu/ui/TesterMenuScreen.kt | 31 ++++------ .../navigation/DefaultTesterRouter.kt | 42 +++++++++++++ .../navigation/InnerTesterRouter.kt | 21 +++++++ .../presentation/navigation/TesterScreen.kt | 10 ++++ settings.gradle.kts | 1 + 35 files changed, 389 insertions(+), 113 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt create mode 100644 app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt rename app/src/main/java/com/tangem/tap/{features/di => proxy}/redux/DaggerGraphMiddleware.kt (85%) rename app/src/main/java/com/tangem/tap/{features/di => proxy}/redux/DaggerGraphReducer.kt (57%) create mode 100644 app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt create mode 100644 features/tester/api/.gitignore create mode 100644 features/tester/api/build.gradle.kts create mode 100644 features/tester/api/src/main/AndroidManifest.xml create mode 100644 features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt delete mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesStateHolder.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt delete mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuStateHolder.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9de8e3ac3e..8214555305 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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() } } @@ -127,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) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 14d367046b..100da06a42 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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() { diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 69fc9f25f6..5b0a0db31b 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -49,10 +49,10 @@ import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation import com.tangem.tap.domain.walletconnect.WalletConnectRepository -import com.tangem.tap.features.di.redux.DaggerGraphAction import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.persistence.PreferencesStorage import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.wallet.BuildConfig import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.launch @@ -166,7 +166,7 @@ class TapApplication : Application(), ImageLoaderFactory { appStateHolder.userTokensRepository = userTokensRepository appStateHolder.walletStoresManager = walletStoresManager - store.dispatch(DaggerGraphAction.SetDependencies(assetReader)) + store.dispatch(DaggerGraphAction.SetApplicationDependencies(assetReader)) scope.launch { featureTogglesManager.init() diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 0a85acf922..498368e943 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -4,7 +4,6 @@ import com.tangem.tap.common.redux.global.globalReducer import com.tangem.tap.common.redux.navigation.NavigationReducer import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer -import com.tangem.tap.features.di.redux.DaggerGraphReducer import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer import com.tangem.tap.features.home.redux.HomeReducer import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer @@ -20,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 { diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index cd4e19b15f..1c837f434e 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -11,8 +11,6 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState -import com.tangem.tap.features.di.redux.DaggerGraphMiddleware -import com.tangem.tap.features.di.redux.DaggerGraphState import com.tangem.tap.features.disclaimer.redux.DisclaimerMiddleware import com.tangem.tap.features.disclaimer.redux.DisclaimerState import com.tangem.tap.features.home.redux.HomeMiddleware @@ -43,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 diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index 88d83b897c..574363c770 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 051a9148f2..dcae84602f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -39,6 +39,7 @@ class DetailsViewModel(private val store: Store) { 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) { SettingsElement.ReferralProgram -> { store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) } + SettingsElement.TesterMenu -> { + store.state.daggerGraphState.testerRouter?.startTesterScreen() + } } } diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt deleted file mode 100644 index 6019e1d5b3..0000000000 --- a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphAction.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.features.di.redux - -import com.tangem.datasource.asset.AssetReader -import org.rekotlin.Action - -sealed interface DaggerGraphAction : Action { - - data class SetDependencies(val assetReader: AssetReader) : DaggerGraphAction -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt deleted file mode 100644 index fe726f8dc0..0000000000 --- a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.tap.features.di.redux - -import com.tangem.datasource.asset.AssetReader -import org.rekotlin.StateType - -data class DaggerGraphState(val assetReader: AssetReader? = null) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt new file mode 100644 index 0000000000..f8e0d379ef --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.proxy.redux + +import com.tangem.datasource.asset.AssetReader +import com.tangem.features.tester.api.TesterRouter +import org.rekotlin.Action + +sealed interface DaggerGraphAction : Action { + + data class SetApplicationDependencies(val assetReader: AssetReader) : DaggerGraphAction + + data class SetActivityDependencies(val testerRouter: TesterRouter) : DaggerGraphAction +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphMiddleware.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphMiddleware.kt similarity index 85% rename from app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphMiddleware.kt rename to app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphMiddleware.kt index d88f8da24e..ee884e4e2a 100644 --- a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphMiddleware.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.di.redux +package com.tangem.tap.proxy.redux import com.tangem.tap.common.redux.AppState import org.rekotlin.Middleware diff --git a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt similarity index 57% rename from app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphReducer.kt rename to app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index f060e845f9..54988a4aa2 100644 --- a/app/src/main/java/com/tangem/tap/features/di/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.di.redux +package com.tangem.tap.proxy.redux import com.tangem.tap.common.redux.AppState import org.rekotlin.Action @@ -12,8 +12,11 @@ object DaggerGraphReducer { private fun internalReduce(action: DaggerGraphAction, state: AppState): DaggerGraphState { return when (action) { - is DaggerGraphAction.SetDependencies -> state.daggerGraphState.copy( - assetReader = action.assetReader, + is DaggerGraphAction.SetApplicationDependencies -> state.daggerGraphState.copy( + assetReader = action.assetReader + ) + is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy( + testerRouter = action.testerRouter ) } } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt new file mode 100644 index 0000000000..ceea2d495c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.proxy.redux + +import com.tangem.datasource.asset.AssetReader +import com.tangem.features.tester.api.TesterRouter +import org.rekotlin.StateType + +data class DaggerGraphState( + val assetReader: AssetReader? = null, + val testerRouter: TesterRouter? = null, +) : StateType \ No newline at end of file diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt index f739b70479..14999b1c51 100644 --- a/buildSrc/src/main/java/Dependency.kt +++ b/buildSrc/src/main/java/Dependency.kt @@ -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 diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index f1e23c39a8..b4eefa618b 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -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 diff --git a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt index ab449b92ec..7cebaee8a8 100644 --- a/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt +++ b/core/featuretoggles/src/main/java/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt @@ -16,7 +16,12 @@ internal class DefaultVersionProvider @Inject constructor( ) : VersionProvider { override fun get(): String? { - val versionName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + 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, @@ -26,9 +31,6 @@ internal class DefaultVersionProvider @Inject constructor( } else { context.packageManager.getPackageInfo(context.packageName, 0).versionName } - - return runCatching { versionName.substringBefore(VERSION_NAME_DELIMITER) } - .fold(onSuccess = { it }, onFailure = { null }) } private companion object { diff --git a/features/tester/api/.gitignore b/features/tester/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/tester/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/tester/api/build.gradle.kts b/features/tester/api/build.gradle.kts new file mode 100644 index 0000000000..12beb12279 --- /dev/null +++ b/features/tester/api/build.gradle.kts @@ -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() + } + } +} \ No newline at end of file diff --git a/features/tester/api/src/main/AndroidManifest.xml b/features/tester/api/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..99ac562598 --- /dev/null +++ b/features/tester/api/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt new file mode 100644 index 0000000000..913c04a076 --- /dev/null +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt @@ -0,0 +1,12 @@ +package com.tangem.features.tester.api + +/** + * Outer tester feature router + * +[REDACTED_AUTHOR] + */ +interface TesterRouter { + + /** Open tester menu */ + fun startTesterScreen() +} \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index d32128a56b..32807a6ded 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -46,7 +46,7 @@ android { Field.Environment("release"), Field.TestActionEnabled(true), Field.LogEnabled(true), - Field.TesterMenuAvailability(true) + Field.TesterMenuAvailability(true), ), builder = ::buildConfigField, ).create() @@ -67,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")) } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt new file mode 100644 index 0000000000..433341f7f7 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt @@ -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 +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index fb1f0e1777..f90be893ca 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -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().apply { + setupNavigation(innerTesterRouter) + } + + FeatureTogglesScreen(state = viewModel.uiState) + } } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/models/TesterFeatureToggle.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/models/TesterFeatureToggle.kt index b898f31db6..782c2a7714 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/models/TesterFeatureToggle.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/models/TesterFeatureToggle.kt @@ -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, ) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt new file mode 100644 index 0000000000..4c55258e0d --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt @@ -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, + val onBackClick: () -> Unit, + val onToggleValueChange: (String, Boolean) -> Unit, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesStateHolder.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesStateHolder.kt deleted file mode 100644 index 5dea9cf196..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesStateHolder.kt +++ /dev/null @@ -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, - val onToggleValueChange: (String, Boolean) -> Unit, - ) : FeatureTogglesStateHolder -} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt index 6d9680804e..c64548832a 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt @@ -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 = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt new file mode 100644 index 0000000000..4c10030524 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -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 { + return this + .getFeatureToggles() + .map { TesterFeatureToggle(it.key, it.value) } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt new file mode 100644 index 0000000000..b99bc61405 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt @@ -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, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuStateHolder.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuStateHolder.kt deleted file mode 100644 index b2afffb27b..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuStateHolder.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt index 08b73b4e26..ac0c7d15d4 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt @@ -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 = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt new file mode 100644 index 0000000000..97126f3540 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt @@ -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() + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt new file mode 100644 index 0000000000..0d5468767f --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt @@ -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() +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt new file mode 100644 index 0000000000..18934aa128 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.tester.presentation.navigation + +/** + * Tester feature screens + * +[REDACTED_AUTHOR] + */ +internal enum class TesterScreen { + MENU, FEATURE_TOGGLES +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index da8fd1aacc..8083954d2b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,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 \ No newline at end of file From 17ba4a88e3fb502019fc052989db05cb35eb007f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Mar 2023 18:28:31 +0800 Subject: [PATCH 4/4] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 14 +- .../tap/common/extensions/WalletManager.kt | 7 +- .../tap/domain/tokens/UserTokensRepository.kt | 12 +- .../DefaultWalletAmountsRepository.kt | 144 ++++++++---------- .../features/tokens/redux/TokensMiddleware.kt | 3 +- .../redux/middlewares/WalletMiddleware.kt | 20 +-- .../redux/middlewares/WarningsMiddleware.kt | 16 +- .../tangem/tap/network/NetworkConnectivity.kt | 71 --------- .../tap/proxy/redux/DaggerGraphAction.kt | 6 +- .../tap/proxy/redux/DaggerGraphReducer.kt | 5 +- .../tap/proxy/redux/DaggerGraphState.kt | 11 +- .../AndroidNetworkConnectionManager.kt | 19 +-- .../datasource/connection/ConnectionStatus.kt | 10 -- .../connection/NetworkConnectionManager.kt | 4 +- 14 files changed, 127 insertions(+), 215 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/network/NetworkConnectivity.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/connection/ConnectionStatus.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 5b0a0db31b..2f558982d2 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -17,6 +17,7 @@ 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.connection.NetworkConnectionManager import com.tangem.domain.DomainLayer import com.tangem.domain.common.LogConfig import com.tangem.tap.common.IntentHandler @@ -49,7 +50,6 @@ 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 @@ -124,6 +124,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var featureTogglesManager: FeatureTogglesManager + @Inject + lateinit var networkConnectionManager: NetworkConnectionManager + override fun onCreate() { super.onCreate() @@ -144,7 +147,6 @@ class TapApplication : Application(), ImageLoaderFactory { registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) DomainLayer.init() - NetworkConnectivity.createInstance(store, this) preferencesStorage = PreferencesStorage(this) walletConnectRepository = WalletConnectRepository(this) @@ -161,12 +163,18 @@ 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(DaggerGraphAction.SetApplicationDependencies(assetReader)) + store.dispatch( + action = DaggerGraphAction.SetApplicationDependencies( + assetReader = assetReader, + networkConnectionManager = networkConnectionManager, + ), + ) scope.launch { featureTogglesManager.init() diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 0939e05be9..0930704d50 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -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 = 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 = wallet.getTxHistory(currency) - fun WalletManager.getBlockchainTxHistory(): List = wallet.getBlockchainTxHistory() fun WalletManager.getTokenTxHistory(token: Token): List = wallet.getTokenTxHistory(token) diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index 03b431d3e2..ee21bf9347 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -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, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt index f42dd9ced5..d2936d9dcc 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt @@ -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, @@ -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?, fiatCurrency: FiatCurrency, ): CompletionResult { - // 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, ): CompletionResult = 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, ): CompletionResult = 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 { - 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): List { - 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() + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 9d741424b7..305df3393e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -31,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 @@ -84,7 +85,7 @@ class TokensMiddleware { val loadCoinsService = LoadAvailableCoinsService( tangemTechApi = store.state.domainNetworks.tangemTechService.api, dispatchers = AppCoroutineDispatcherProvider(), - assetReader = requireNotNull(store.state.daggerGraphState.assetReader) { "Asset reader is null" }, + assetReader = store.state.daggerGraphState.get(DaggerGraphState::assetReader), ) scope.launch { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 1866921b7e..41b72a10d4 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -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 = { _, 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 } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index d9a40e965d..1de96bac4b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/network/NetworkConnectivity.kt b/app/src/main/java/com/tangem/tap/network/NetworkConnectivity.kt deleted file mode 100644 index bf61064eaa..0000000000 --- a/app/src/main/java/com/tangem/tap/network/NetworkConnectivity.kt +++ /dev/null @@ -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 = 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 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt index f8e0d379ef..cd392ae831 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -1,12 +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) : DaggerGraphAction + data class SetApplicationDependencies( + val assetReader: AssetReader, + val networkConnectionManager: NetworkConnectionManager, + ) : DaggerGraphAction data class SetActivityDependencies(val testerRouter: TesterRouter) : DaggerGraphAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index 54988a4aa2..34564599ac 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -13,10 +13,11 @@ object DaggerGraphReducer { private fun internalReduce(action: DaggerGraphAction, state: AppState): DaggerGraphState { return when (action) { is DaggerGraphAction.SetApplicationDependencies -> state.daggerGraphState.copy( - assetReader = action.assetReader + assetReader = action.assetReader, + networkConnectionManager = action.networkConnectionManager, ) is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy( - testerRouter = action.testerRouter + testerRouter = action.testerRouter, ) } } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index ceea2d495c..800182a67c 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -1,10 +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, -) : StateType \ No newline at end of file + val networkConnectionManager: NetworkConnectionManager? = null, +) : StateType { + + inline fun get(getDependency: DaggerGraphState.() -> T?): T { + return requireNotNull(getDependency()) { + "${T::class.simpleName} isn't initialized " + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt b/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt index d5bff89d3b..695dbce8bd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt @@ -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 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 { diff --git a/core/datasource/src/main/java/com/tangem/datasource/connection/ConnectionStatus.kt b/core/datasource/src/main/java/com/tangem/datasource/connection/ConnectionStatus.kt deleted file mode 100644 index b59aa6f4df..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/connection/ConnectionStatus.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.datasource.connection - -/** - * Network connection status - * -[REDACTED_AUTHOR] - */ -enum class ConnectionStatus { - ONLINE, OFFLINE -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt b/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt index 9d6c73849d..fa6faca57d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt @@ -1,10 +1,8 @@ package com.tangem.datasource.connection -import kotlinx.coroutines.flow.StateFlow - /** Network connection manager */ interface NetworkConnectionManager { /** Connection status */ - val connectionStatus: StateFlow + val isOnline: Boolean } \ No newline at end of file