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