Updated on 2026-08-14
This commit is contained in:
parent
1f27edeb70
commit
da1b0477e0
9 changed files with 114 additions and 1 deletions
|
|
@ -102,7 +102,7 @@ The app uses [Decompose](https://github.com/arkivanov/Decompose) for lifecycle-a
|
|||
- **Async:** Kotlin Coroutines + Flow. Inject `CoroutineDispatcherProvider` (from `core/utils`) instead of using `Dispatchers.*` directly — provides `main`, `mainImmediate`, `io`, `default`, `single`
|
||||
- **Error handling:** Arrow's `Either<Error, Success>` pattern throughout domain/data layers. `DataError` sealed hierarchy for domain errors. See `domain/core/CLAUDE.md` for the LCE pattern
|
||||
- **Analytics:** `AnalyticsEvent(category, event, params)` in `core/analytics/models/`. Feature events are sealed class hierarchies extending `AnalyticsEvent`. Send via injected `AnalyticsEventHandler`
|
||||
- **Feature toggles:** `FeatureTogglesManager` in `core/config-toggles/`. Toggles are defined in `core/config-toggles/src/main/assets/configs/feature_toggles_config.json` and auto-generated into a `FeatureToggles` enum by the convention plugin at build time. Each feature module exposes its own `XxxFeatureToggles` interface (in `api/`) with a `DefaultXxxFeatureToggles` implementation (in `impl/`) that delegates to `FeatureTogglesManager`
|
||||
- **Feature toggles:** `FeatureTogglesManager` in `core/config-toggles/`. See `core/config-toggles/CLAUDE.md`.
|
||||
- **Supported languages:** `SupportedLanguages` in `core/utils/` defines the app's supported locales: en, ru, de, fr, it, ja, uk, zh, es. `getCurrentSupportedLanguageCode()` returns the device locale if supported, otherwise falls back to English. Used by API calls that accept a language parameter
|
||||
|
||||
### Build System
|
||||
|
|
|
|||
70
core/config-toggles/CLAUDE.md
Normal file
70
core/config-toggles/CLAUDE.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# core/config-toggles
|
||||
|
||||
Feature toggles (and the related excluded-blockchains toggles). Toggles gate
|
||||
features by app version; the JSON config is the source of truth and the
|
||||
`FeatureToggles` enum is generated from it at build time.
|
||||
|
||||
## How it works
|
||||
|
||||
- **Config:** `src/main/assets/configs/feature_toggles_config.json` — a JSON array
|
||||
of `{ "name": <STRING>, "version": <STRING> }` (`ConfigToggle`).
|
||||
- The **convention plugin** generates the `FeatureToggles` enum (one entry per
|
||||
`name`) at build time. Reference it as `FeatureToggles.<NAME>`.
|
||||
- **Entry point:** `FeatureTogglesManager.isFeatureEnabled(FeatureToggles.X)`.
|
||||
- `ProdFeatureTogglesManager` (release): a toggle is enabled when the app
|
||||
version `>=` its `version`.
|
||||
- `DevFeatureTogglesManager` (tester builds, `BuildConfig.TESTER_MENU_ENABLED`):
|
||||
runtime-toggleable via the Tester Menu.
|
||||
- **`version` semantics:**
|
||||
- `"undefined"` (`DISABLED_FEATURE_TOGGLE_VERSION`) → OFF in prod; can only be
|
||||
flipped ON via the Tester Menu / dev builds. Use this while a feature is in
|
||||
development.
|
||||
- `"X.Y"` (e.g. `5.40`) → ON in prod from that app version onward
|
||||
(`currentVersion >= localVersion`, see `VersionAvailabilityContract`).
|
||||
|
||||
## Naming convention (ENFORCED by a test)
|
||||
|
||||
- A toggle `name` MUST match `^(AND|TWI)_\d+(?:_[A-Z0-9]+)+$` — start with the
|
||||
Jira ticket id (`AND_<id>` for Android tickets, `TWI_<id>` for idea tickets),
|
||||
then an `UPPER_SNAKE_CASE` suffix. Example: `AND_15901_STORIES_CONTAINER_ENABLED`.
|
||||
- Enforced by `FeatureTogglesNamingConventionTest`. Legacy toggles that predate
|
||||
the rule are whitelisted in its `EXCLUDED_TOGGLES_LIST` — do **not** add new
|
||||
names there without an explicit reason.
|
||||
- The Kotlin interface property stays human-readable **without** the ticket id:
|
||||
`isStoriesContainerEnabled`.
|
||||
|
||||
## Per-feature toggles & how to add one
|
||||
|
||||
Each feature owns its toggles — feature code reads them through its own
|
||||
interface, never `FeatureTogglesManager` directly:
|
||||
|
||||
- `api/`: `XxxFeatureToggles` interface — `val isYyyEnabled: Boolean`.
|
||||
- `impl/`: `DefaultXxxFeatureToggles(featureTogglesManager)` exposes each toggle as
|
||||
a **getter-backed property**, not a stored value — so it is re-evaluated on every
|
||||
read (required for runtime toggling via the Tester Menu):
|
||||
|
||||
```kotlin
|
||||
override val isYyyEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_<id>_YYY)
|
||||
```
|
||||
|
||||
Never `val isYyyEnabled = featureTogglesManager.isFeatureEnabled(...)` (evaluated
|
||||
once at construction).
|
||||
- DI: a `@Provides @Singleton` in the feature's Hilt module returning the interface.
|
||||
|
||||
To add a toggle:
|
||||
|
||||
1. Add `{ "name": "AND_<id>_FOO_ENABLED", "version": "undefined" }` to the config
|
||||
JSON (the enum is regenerated at build).
|
||||
2. Add `val isFooEnabled` to the feature's `XxxFeatureToggles` and map it in
|
||||
`DefaultXxxFeatureToggles` (create the interface/impl/DI provider if the
|
||||
feature has none yet).
|
||||
3. Gate code on `xxxFeatureToggles.isFooEnabled`.
|
||||
|
||||
## Removing (cleanup)
|
||||
|
||||
When a toggle ships at 100%, set its `version` to the release and run the
|
||||
`cleanup-feature-toggles` skill — it removes the JSON entry, the interface/impl
|
||||
members, inlines `true`, and drops dead branches. Mark code that must be deleted
|
||||
together with a toggle using `@RemoveWithToggle("AND_<id>_FOO_ENABLED")`
|
||||
(`com.tangem.utils.annotations.RemoveWithToggle`); the cleanup skill picks it up.
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
[
|
||||
{
|
||||
"name": "AND_15901_STORIES_CONTAINER_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEW_CARD_SCANNING_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.home.api
|
||||
|
||||
interface HomeFeatureToggles {
|
||||
|
||||
val isStoriesContainerEnabled: Boolean
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.configToggles)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.routing)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.home.impl
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.home.api.HomeFeatureToggles
|
||||
|
||||
internal class DefaultHomeFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : HomeFeatureToggles {
|
||||
|
||||
override val isStoriesContainerEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_15901_STORIES_CONTAINER_ENABLED)
|
||||
}
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
package com.tangem.features.home.impl.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.home.api.HomeComponent
|
||||
import com.tangem.features.home.api.HomeFeatureToggles
|
||||
import com.tangem.features.home.impl.DefaultHomeComponent
|
||||
import com.tangem.features.home.impl.DefaultHomeFeatureToggles
|
||||
import com.tangem.features.home.impl.model.HomeModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
|
|
@ -22,6 +26,17 @@ internal interface ComponentModule {
|
|||
fun bindComponent(factory: DefaultHomeComponent.Factory): HomeComponent.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object HomeFeatureTogglesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHomeFeatureToggles(featureTogglesManager: FeatureTogglesManager): HomeFeatureToggles {
|
||||
return DefaultHomeFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
|||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.feature.referral.domain.ShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.features.home.api.HomeComponent
|
||||
import com.tangem.features.home.api.HomeFeatureToggles
|
||||
import com.tangem.features.home.impl.ui.state.HomeUM
|
||||
import com.tangem.features.home.impl.ui.state.Stories
|
||||
import com.tangem.features.home.impl.ui.state.getRestrictedStories
|
||||
|
|
@ -67,6 +68,7 @@ internal class HomeModel @Inject constructor(
|
|||
private val urlOpener: UrlOpener,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val shouldShowMobileWalletPromoUseCase: ShouldShowMobileWalletPromoUseCase,
|
||||
private val homeFeatureToggles: HomeFeatureToggles,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -77,6 +79,7 @@ internal class HomeModel @Inject constructor(
|
|||
private val _uiState = MutableStateFlow(
|
||||
HomeUM(
|
||||
scanInProgress = false,
|
||||
isStoriesContainerEnabled = homeFeatureToggles.isStoriesContainerEnabled,
|
||||
stories = getRestrictedStories().toImmutableList(),
|
||||
onShopClick = ::onShopClick,
|
||||
onSearchTokensClick = ::onSearchTokensClick,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
|
||||
data class HomeUM(
|
||||
val scanInProgress: Boolean,
|
||||
val isStoriesContainerEnabled: Boolean,
|
||||
val stories: ImmutableList<Stories>,
|
||||
val onShopClick: () -> Unit,
|
||||
val onSearchTokensClick: () -> Unit,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue